diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 102031c1e..402e44597 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -517,6 +517,13 @@
+
+
+
+
diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java
index e301bd74c..fdfe675d6 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/App.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/App.java
@@ -569,6 +569,10 @@ public class App extends ValueObject implements Comparable, 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;
}
diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java
index 349f71fa0..122885612 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java
@@ -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) {
diff --git a/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java b/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java
index d5461799f..5c32395a7 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java
@@ -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());
- }
}
diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java
index 5f15a83c0..8d590540e 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java
+++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java
@@ -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,28 +81,32 @@ public class AppListItemController extends RecyclerView.ViewHolder {
this.activity = activity;
installButton = (ImageView) itemView.findViewById(R.id.install);
- installButton.setOnClickListener(onInstallClicked);
+ if (installButton != null) {
+ installButton.setOnClickListener(onInstallClicked);
- if (Build.VERSION.SDK_INT >= 21) {
- installButton.setOutlineProvider(new ViewOutlineProvider() {
- @Override
- public void getOutline(View view, Outline outline) {
- float density = activity.getResources().getDisplayMetrics().density;
+ if (Build.VERSION.SDK_INT >= 21) {
+ installButton.setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ float density = activity.getResources().getDisplayMetrics().density;
- // TODO: This is a bit hacky/hardcoded/too-specific to the particular icons we're using.
- // This is because the default "download & install" and "downloaded & ready to install"
- // icons are smaller than the "downloading progress" button. Hence, we can't just use
- // the width/height of the view to calculate the outline size.
- int xPadding = (int) (8 * density);
- int yPadding = (int) (9 * density);
- outline.setOval(xPadding, yPadding, installButton.getWidth() - xPadding, installButton.getHeight() - yPadding);
- }
- });
+ // TODO: This is a bit hacky/hardcoded/too-specific to the particular icons we're using.
+ // This is because the default "download & install" and "downloaded & ready to install"
+ // icons are smaller than the "downloading progress" button. Hence, we can't just use
+ // the width/height of the view to calculate the outline size.
+ int xPadding = (int) (8 * density);
+ int yPadding = (int) (9 * density);
+ outline.setOval(xPadding, yPadding, installButton.getWidth() - xPadding, installButton.getHeight() - yPadding);
+ }
+ });
+ }
}
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) {
diff --git a/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppsActivity.java
new file mode 100644
index 000000000..6f051dd6d
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppsActivity.java
@@ -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 {
+
+ 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 onCreateLoader(int id, Bundle args) {
+ return new CursorLoader(
+ this,
+ AppProvider.getInstalledUri(),
+ Schema.AppMetadataTable.Cols.ALL,
+ null, null, null);
+ }
+
+ @Override
+ public void onLoadFinished(Loader loader, Cursor cursor) {
+ adapter.setApps(cursor);
+ }
+
+ @Override
+ public void onLoaderReset(Loader loader) {
+ adapter.setApps(null);
+ }
+
+ static class InstalledAppListAdapter extends RecyclerView.Adapter {
+
+ 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();
+ }
+ }
+}
diff --git a/app/src/main/res/layout/installed_app_list_item.xml b/app/src/main/res/layout/installed_app_list_item.xml
new file mode 100644
index 000000000..764e202db
--- /dev/null
+++ b/app/src/main/res/layout/installed_app_list_item.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/installed_apps_layout.xml b/app/src/main/res/layout/installed_apps_layout.xml
new file mode 100644
index 000000000..1d417ff14
--- /dev/null
+++ b/app/src/main/res/layout/installed_apps_layout.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/main_activity_screens.xml b/app/src/main/res/menu/main_activity_screens.xml
index 66e0feb76..5573400ce 100644
--- a/app/src/main/res/menu/main_activity_screens.xml
+++ b/app/src/main/res/menu/main_activity_screens.xml
@@ -17,7 +17,7 @@
app:showAsAction="ifRoom|withText"
android:id="@+id/nearby" />
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5000f3f61..b7bdc2516 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -65,6 +65,11 @@
Installed (from unknown source)
Version %1$s available
Version %1$s
+ Version %1$s (Recommended)
+
+ Installed Apps
+ Updates ignored
+ Updates ignored for Version %1$s
Added on %s
@@ -142,7 +147,9 @@
Latest
Categories
Nearby
- My Apps
+
+ My Apps
+ Manage Installed Apps
Version %s installed
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index fd75a1db9..fe052ef5b 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -1,6 +1,12 @@
-
+
+
+
+
@@ -9,6 +15,8 @@
android:targetPackage="org.fdroid.fdroid"
android:targetClass="org.fdroid.fdroid.views.ManageReposActivity" />
+
+