Added "Installed Apps" activity to top of settings.

This is as per the mockup in issue #840, and does the following:

 * Adds a new `PreferencesCategory` of "My Apps" at the top of the
   preferences screen.
 * Adds a "Manage Installed Apps" preference, and moves the
   "Repositories" preference into this category.
 * Repeals an existing change which prevented "updateable" apps from
   appearing in the list of "installed" apps. This is because the two
   lists of apps are no longer displayed alongside eachother.
 * Enhances the `AppListItemController` to also be able to display
   whether or not the currently installed version is the recommended
   version or not.
 * Also adds option to display whether the user has asked to ignore any
   updates for any specific apps.
This commit is contained in:
Peter Serwylo 2017-02-14 09:07:00 +11:00
parent 92943ebdf3
commit a1a7427cd2
12 changed files with 382 additions and 46 deletions

View File

@ -517,6 +517,13 @@
<activity android:name=".views.apps.AppListActivity" />
<activity android:name=".views.installed.InstalledAppsActivity"
android:parentActivityName=".views.main.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".views.main.MainActivity" />
</activity>
</application>
</manifest>

View File

@ -569,6 +569,10 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
return TextUtils.isEmpty(flattrID) ? null : "https://flattr.com/thing/" + flattrID;
}
/**
* @see App#suggestedVersionName for why this uses a getter while other member variables are
* publicly accessible.
*/
public String getSuggestedVersionName() {
return suggestedVersionName;
}

View File

@ -199,26 +199,16 @@ public class AppProvider extends FDroidProvider {
public AppQuerySelection add(AppQuerySelection query) {
QuerySelection both = super.add(query);
AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs());
ensureJoinsCopied(query, bothWithJoin);
if (this.naturalJoinToInstalled() || query.naturalJoinToInstalled()) {
bothWithJoin.requireNaturalInstalledTable();
}
if (this.leftJoinToPrefs() || query.leftJoinToPrefs()) {
bothWithJoin.requireLeftJoinPrefs();
}
return bothWithJoin;
}
public AppQuerySelection not(AppQuerySelection query) {
QuerySelection both = super.not(query);
AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs());
ensureJoinsCopied(query, bothWithJoin);
return bothWithJoin;
}
private void ensureJoinsCopied(AppQuerySelection toAdd, AppQuerySelection newlyCreated) {
if (this.naturalJoinToInstalled() || toAdd.naturalJoinToInstalled()) {
newlyCreated.requireNaturalInstalledTable();
}
if (this.leftJoinToPrefs() || toAdd.leftJoinToPrefs()) {
newlyCreated.requireLeftJoinPrefs();
}
}
}
protected class Query extends QueryBuilder {
@ -574,8 +564,7 @@ public class AppProvider extends FDroidProvider {
final String ignoreAll = "COALESCE(prefs." + AppPrefsTable.Cols.IGNORE_ALL_UPDATES + ", 0) != 1";
final String ignore = " (" + ignoreCurrent + " AND " + ignoreAll + ") ";
final String nullChecks = app + "." + Cols.SUGGESTED_VERSION_CODE + " IS NOT NULL AND installed." + InstalledAppTable.Cols.VERSION_CODE + " IS NOT NULL ";
final String where = nullChecks + " AND " + ignore + " AND " + app + "." + Cols.SUGGESTED_VERSION_CODE + " > installed." + InstalledAppTable.Cols.VERSION_CODE;
final String where = ignore + " AND " + app + "." + Cols.SUGGESTED_VERSION_CODE + " > installed." + InstalledAppTable.Cols.VERSION_CODE;
return new AppQuerySelection(where).requireNaturalInstalledTable().requireLeftJoinPrefs();
}
@ -587,7 +576,7 @@ public class AppProvider extends FDroidProvider {
}
private AppQuerySelection queryInstalled() {
return new AppQuerySelection().requireNaturalInstalledTable().not(queryCanUpdate());
return new AppQuerySelection().requireNaturalInstalledTable();
}
private AppQuerySelection querySearch(String query) {

View File

@ -78,8 +78,4 @@ public class QuerySelection {
return new QuerySelection(s, a);
}
public QuerySelection not(QuerySelection querySelection) {
String where = " NOT (" + querySelection.getSelection() + ") ";
return add(where, querySelection.getArgs());
}
}

View File

@ -11,6 +11,7 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
@ -33,6 +34,7 @@ import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppPrefs;
import org.fdroid.fdroid.installer.ApkCache;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
@ -50,10 +52,24 @@ public class AppListItemController extends RecyclerView.ViewHolder {
private final Activity activity;
private final ImageView installButton;
@NonNull
private final ImageView icon;
@NonNull
private final TextView name;
@Nullable
private final ImageView installButton;
@Nullable
private final TextView status;
@Nullable
private final TextView installedVersion;
@Nullable
private final TextView ignoredStatus;
private final DisplayImageOptions displayImageOptions;
private App currentApp;
@ -65,6 +81,7 @@ public class AppListItemController extends RecyclerView.ViewHolder {
this.activity = activity;
installButton = (ImageView) itemView.findViewById(R.id.install);
if (installButton != null) {
installButton.setOnClickListener(onInstallClicked);
if (Build.VERSION.SDK_INT >= 21) {
@ -83,10 +100,13 @@ public class AppListItemController extends RecyclerView.ViewHolder {
}
});
}
}
icon = (ImageView) itemView.findViewById(R.id.icon);
name = (TextView) itemView.findViewById(R.id.app_name);
status = (TextView) itemView.findViewById(R.id.status);
installedVersion = (TextView) itemView.findViewById(R.id.installed_version);
ignoredStatus = (TextView) itemView.findViewById(R.id.ignored_status);
displayImageOptions = Utils.getImageLoadingOptions().build();
@ -110,6 +130,8 @@ public class AppListItemController extends RecyclerView.ViewHolder {
broadcastManager.registerReceiver(onInstallAction, Installer.getInstallIntentFilter(Uri.parse(currentAppDownloadUrl)));
configureStatusText(app);
configureInstalledVersion(app);
configureIgnoredStatus(app);
configureInstallButton(app);
}
@ -145,6 +167,42 @@ public class AppListItemController extends RecyclerView.ViewHolder {
}
/**
* Shows the currently installed version name, and whether or not it is the recommended version.
* Binds to the {@link R.id#installed_version} {@link TextView}.
*/
private void configureInstalledVersion(@NonNull App app) {
if (installedVersion == null) {
return;
}
int res = (app.suggestedVersionCode == app.installedVersionCode)
? R.string.app_recommended_version_installed : R.string.app_version_x_installed;
installedVersion.setText(activity.getString(res, app.installedVersionName));
}
/**
* Shows whether the user has previously asked to ignore updates for this app entirely, or for a
* specific version of this app. Binds to the {@link R.id#ignored_status} {@link TextView}.
*/
private void configureIgnoredStatus(@NonNull App app) {
if (ignoredStatus == null) {
return;
}
AppPrefs prefs = app.getPrefs(activity);
if (prefs.ignoreAllUpdates) {
ignoredStatus.setText(activity.getString(R.string.installed_app__updates_ignored));
ignoredStatus.setVisibility(View.VISIBLE);
} else if (prefs.ignoreThisUpdate > 0 && prefs.ignoreThisUpdate == app.suggestedVersionCode) {
ignoredStatus.setText(activity.getString(R.string.installed_app__updates_ignored_for_suggested_version, app.getSuggestedVersionName()));
ignoredStatus.setVisibility(View.VISIBLE);
} else {
ignoredStatus.setVisibility(View.GONE);
}
}
private boolean isReadyToInstall(@NonNull App app) {
for (AppUpdateStatusManager.AppUpdateStatus appStatus : AppUpdateStatusManager.getInstance(activity).getByPackageName(app.packageName)) {
if (appStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) {
@ -184,6 +242,7 @@ public class AppListItemController extends RecyclerView.ViewHolder {
}
}
@SuppressWarnings("FieldCanBeLocal")
private final View.OnClickListener onAppClicked = new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -206,7 +265,7 @@ public class AppListItemController extends RecyclerView.ViewHolder {
private final BroadcastReceiver onDownloadProgress = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (currentApp == null || !TextUtils.equals(currentAppDownloadUrl, intent.getDataString())) {
if (installButton == null || currentApp == null || !TextUtils.equals(currentAppDownloadUrl, intent.getDataString())) {
return;
}
@ -226,7 +285,7 @@ public class AppListItemController extends RecyclerView.ViewHolder {
private final BroadcastReceiver onInstallAction = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (currentApp == null) {
if (currentApp == null || installButton == null) {
return;
}
@ -248,6 +307,7 @@ public class AppListItemController extends RecyclerView.ViewHolder {
}
};
@SuppressWarnings("FieldCanBeLocal")
private final View.OnClickListener onInstallClicked = new View.OnClickListener() {
@Override
public void onClick(View v) {

View File

@ -0,0 +1,143 @@
/*
* Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com
* Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.fdroid.fdroid.views.installed;
import android.app.Activity;
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.ViewGroup;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.views.apps.AppListItemController;
public class InstalledAppsActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {
private InstalledAppListAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
((FDroidApp) getApplication()).applyTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.installed_apps_layout);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.setTitle(getString(R.string.installed_apps__activity_title));
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
adapter = new InstalledAppListAdapter(this);
RecyclerView appList = (RecyclerView) findViewById(R.id.app_list);
appList.setHasFixedSize(true);
appList.setLayoutManager(new LinearLayoutManager(this));
appList.setAdapter(adapter);
}
@Override
protected void onResume() {
super.onResume();
// Starts a new or restarts an existing Loader in this manager
getSupportLoaderManager().restartLoader(0, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(
this,
AppProvider.getInstalledUri(),
Schema.AppMetadataTable.Cols.ALL,
null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
adapter.setApps(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.setApps(null);
}
static class InstalledAppListAdapter extends RecyclerView.Adapter<AppListItemController> {
private final Activity activity;
@Nullable
private Cursor cursor;
InstalledAppListAdapter(Activity activity) {
this.activity = activity;
setHasStableIds(true);
}
@Override
public long getItemId(int position) {
if (cursor == null) {
return 0;
}
cursor.moveToPosition(position);
return cursor.getLong(cursor.getColumnIndex(Schema.AppMetadataTable.Cols.ROW_ID));
}
@Override
public AppListItemController onCreateViewHolder(ViewGroup parent, int viewType) {
View view = activity.getLayoutInflater().inflate(R.layout.installed_app_list_item, parent, false);
return new AppListItemController(activity, view);
}
@Override
public void onBindViewHolder(AppListItemController holder, int position) {
if (cursor == null) {
return;
}
cursor.moveToPosition(position);
holder.bindModel(new App(cursor));
}
@Override
public int getItemCount() {
return cursor == null ? 0 : cursor.getCount();
}
public void setApps(@Nullable Cursor cursor) {
this.cursor = cursor;
notifyDataSetChanged();
}
}
}

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<!-- Ignore ContentDescription because it is kind of meaningless to have TTS read out "App icon"
when it will inevitably read out the name of the app straight after. -->
<ImageView
android:id="@+id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="48dp"
android:layout_height="48dp"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="F-Droid Application manager with a long name that will wrap and then ellipsize"
android:textSize="18sp"
android:textColor="#424242"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="@+id/icon"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp" />
<TextView
android:id="@+id/installed_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="Version 4.7.3 (recommended)"
android:textStyle="italic"
android:textSize="14sp"
android:textColor="#424242"
android:maxLines="1"
android:ellipsize="end"
android:fontFamily="sans-serif-light"
app:layout_constraintTop_toBottomOf="@+id/app_name"
app:layout_constraintStart_toEndOf="@+id/icon"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" />
<TextView
android:id="@+id/ignored_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="Updates ignored"
android:textSize="14sp"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@+id/installed_version"
app:layout_constraintStart_toEndOf="@+id/icon"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="368dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:theme="?attr/actionBarTheme"
app:popupTheme="?attr/actionBarPopupTheme" />
<android.support.v7.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/installed_app_list_item"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
android:scrollbars="vertical" />
<!--
Commented out until the long press functionality is implemented.
<TextView
android:id="@+id/helpText"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:padding="16dp"
android:textAlignment="center"
android:textColor="#424242"
android:textSize="14sp"
android:text="Tap and hold on an app for more options"
app:layout_constraintHorizontal_bias="0.0"
android:layout_marginBottom="-1dp" />-->
</android.support.constraint.ConstraintLayout>

View File

@ -17,7 +17,7 @@
app:showAsAction="ifRoom|withText"
android:id="@+id/nearby" />
<item
android:title="@string/main_menu__my_apps"
android:title="@string/preference_category__my_apps"
android:icon="@drawable/ic_my_apps"
app:showAsAction="ifRoom|withText"
android:id="@+id/my_apps" />

View File

@ -65,6 +65,11 @@
<string name="app_inst_unknown_source">Installed (from unknown source)</string>
<string name="app_version_x_available">Version %1$s available</string>
<string name="app_version_x_installed">Version %1$s</string>
<string name="app_recommended_version_installed">Version %1$s (Recommended)</string>
<string name="installed_apps__activity_title">Installed Apps</string>
<string name="installed_app__updates_ignored">Updates ignored</string>
<string name="installed_app__updates_ignored_for_suggested_version">Updates ignored for Version %1$s</string>
<string name="added_on">Added on %s</string>
@ -142,7 +147,9 @@
<string name="main_menu__latest_apps">Latest</string>
<string name="main_menu__categories">Categories</string>
<string name="main_menu__swap_nearby">Nearby</string>
<string name="main_menu__my_apps">My Apps</string>
<string name="preference_category__my_apps">My Apps</string>
<string name="preference_manage_installed_apps">Manage Installed Apps</string>
<string name="details_installed">Version %s installed</string>

View File

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/updates">
<PreferenceCategory android:title="@string/preference_category__my_apps">
<PreferenceScreen android:title="@string/preference_manage_installed_apps">
<intent
android:action="android.intent.action.MAIN"
android:targetPackage="org.fdroid.fdroid"
android:targetClass="org.fdroid.fdroid.views.installed.InstalledAppsActivity" />
</PreferenceScreen>
<PreferenceScreen
android:title="@string/menu_manage"
android:summary="@string/repositories_summary">
@ -9,6 +15,8 @@
android:targetPackage="org.fdroid.fdroid"
android:targetClass="org.fdroid.fdroid.views.ManageReposActivity" />
</PreferenceScreen>
</PreferenceCategory>
<PreferenceCategory android:title="@string/updates">
<com.geecko.QuickLyric.view.AppCompatListPreference android:title="@string/update_interval"
android:key="updateInterval"
android:defaultValue="24"

View File

@ -115,7 +115,7 @@ public class AppProviderTest extends FDroidProviderTest {
assertFalse(notInstalled.canAndWantToUpdate(context));
assertResultCount(contentResolver, 2, AppProvider.getCanUpdateUri(), PROJ);
assertResultCount(contentResolver, 7, AppProvider.getInstalledUri(), PROJ);
assertResultCount(contentResolver, 9, AppProvider.getInstalledUri(), PROJ);
App installedOnlyOneVersionAvailable = AppProvider.Helper.findSpecificApp(r, "installed, only one version available", 1, Cols.ALL);
App installedAlreadyLatestNoIgnore = AppProvider.Helper.findSpecificApp(r, "installed, already latest, no ignore", 1, Cols.ALL);