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" /> + +