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 122885612..349f71fa0 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -199,16 +199,26 @@ public class AppProvider extends FDroidProvider { public AppQuerySelection add(AppQuerySelection query) { QuerySelection both = super.add(query); AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs()); - if (this.naturalJoinToInstalled() || query.naturalJoinToInstalled()) { - bothWithJoin.requireNaturalInstalledTable(); - } - - if (this.leftJoinToPrefs() || query.leftJoinToPrefs()) { - bothWithJoin.requireLeftJoinPrefs(); - } + ensureJoinsCopied(query, bothWithJoin); 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 { @@ -564,7 +574,8 @@ 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 where = ignore + " AND " + app + "." + Cols.SUGGESTED_VERSION_CODE + " > installed." + InstalledAppTable.Cols.VERSION_CODE; + 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; return new AppQuerySelection(where).requireNaturalInstalledTable().requireLeftJoinPrefs(); } @@ -576,7 +587,7 @@ public class AppProvider extends FDroidProvider { } private AppQuerySelection queryInstalled() { - return new AppQuerySelection().requireNaturalInstalledTable(); + return new AppQuerySelection().requireNaturalInstalledTable().not(queryCanUpdate()); } 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 5c32395a7..d5461799f 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java +++ b/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java @@ -78,4 +78,8 @@ 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 new file mode 100644 index 000000000..3da0e8428 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java @@ -0,0 +1,148 @@ +package org.fdroid.fdroid.views.apps; + +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityOptionsCompat; +import android.support.v4.util.Pair; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; + +import org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.ApkProvider; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.installer.InstallManagerService; + +public class AppListItemController extends RecyclerView.ViewHolder { + + private final Activity activity; + + private final Button installButton; + private final ImageView icon; + private final TextView name; + private final TextView status; + private final DisplayImageOptions displayImageOptions; + + private App currentApp; + + public AppListItemController(Activity activity, View itemView) { + super(itemView); + this.activity = activity; + + installButton = (Button) itemView.findViewById(R.id.install); + installButton.setOnClickListener(onInstallClicked); + + icon = (ImageView) itemView.findViewById(R.id.icon); + name = (TextView) itemView.findViewById(R.id.app_name); + status = (TextView) itemView.findViewById(R.id.status); + + displayImageOptions = Utils.getImageLoadingOptions().build(); + + itemView.setOnClickListener(onAppClicked); + } + + public void bindModel(@NonNull App app) { + currentApp = app; + name.setText(Utils.formatAppNameAndSummary(app.name, app.summary)); + + ImageLoader.getInstance().displayImage(app.iconUrl, icon, displayImageOptions); + + configureStatusText(app); + configureInstallButton(app); + } + + /** + * Sets the text/visibility of the {@link R.id#status} {@link TextView} based on whether the app: + * * Is compatible with the users device + * * Is installed + * * Can be updated + * + * TODO: This button also needs to be repurposed to support the "Downloaded but not installed" state. + */ + private void configureStatusText(@NonNull App app) { + if (status == null) { + return; + } + + if (!app.compatible) { + status.setText(activity.getString(R.string.app_incompatible)); + status.setVisibility(View.VISIBLE); + } else if (app.isInstalled()) { + if (app.canAndWantToUpdate(activity)) { + String upgradeFromTo = activity.getString(R.string.app_version_x_available, app.getSuggestedVersionName()); + status.setText(upgradeFromTo); + } else { + String installed = activity.getString(R.string.app_version_x_installed, app.installedVersionName); + status.setText(installed); + } + + status.setVisibility(View.VISIBLE); + } else { + status.setVisibility(View.INVISIBLE); + } + + } + + /** + * The install button is shown when an app: + * * Is compatible with the users device. + * * Has not been filtered due to anti-features/root/etc. + * * Is either not installed or installed but can be updated. + * + * TODO: This button also needs to be repurposed to support the "Downloaded but not installed" state. + */ + private void configureInstallButton(@NonNull App app) { + if (installButton == null) { + return; + } + + boolean installable = app.canAndWantToUpdate(activity) || !app.isInstalled(); + boolean shouldAllow = app.compatible && !app.isFiltered(); + + if (shouldAllow && installable) { + installButton.setVisibility(View.VISIBLE); + } else { + installButton.setVisibility(View.GONE); + } + } + + private final View.OnClickListener onAppClicked = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (currentApp == null) { + return; + } + + Intent intent = new Intent(activity, AppDetails.class); + intent.putExtra(AppDetails.EXTRA_APPID, currentApp.packageName); + if (Build.VERSION.SDK_INT >= 21) { + Pair<View, String> iconTransitionPair = Pair.create((View) icon, activity.getString(R.string.transition_app_item_icon)); + Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, iconTransitionPair).toBundle(); + activity.startActivity(intent, bundle); + } else { + activity.startActivity(intent); + } + } + }; + + private final View.OnClickListener onInstallClicked = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (currentApp == null) { + return; + } + + InstallManagerService.queue(activity, currentApp, ApkProvider.Helper.findApkFromAnyRepo(activity, currentApp.packageName, currentApp.suggestedVersionCode)); + } + }; +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java b/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java index 8d894584c..668a44f9a 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java @@ -8,6 +8,7 @@ import android.widget.Button; import android.widget.FrameLayout; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.views.myapps.MyAppsViewBinder; import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; /** @@ -38,7 +39,11 @@ class MainViewController extends RecyclerView.ViewHolder { new WhatsNewViewBinder(activity, frame); } + /** + * @see MyAppsViewBinder + */ public void bindMyApps() { + new MyAppsViewBinder(activity, frame); } public void bindCategoriesView() { diff --git a/app/src/main/java/org/fdroid/fdroid/views/myapps/InstalledHeaderController.java b/app/src/main/java/org/fdroid/fdroid/views/myapps/InstalledHeaderController.java new file mode 100644 index 000000000..c8adba2ce --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/myapps/InstalledHeaderController.java @@ -0,0 +1,10 @@ +package org.fdroid.fdroid.views.myapps; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class InstalledHeaderController extends RecyclerView.ViewHolder { + public InstalledHeaderController(View itemView) { + super(itemView); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsAdapter.java new file mode 100644 index 000000000..4c29069c8 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsAdapter.java @@ -0,0 +1,76 @@ +package org.fdroid.fdroid.views.myapps; + +import android.app.Activity; +import android.database.Cursor; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.views.apps.AppListItemController; + +/** + * Wraps a cursor which should have a list of "apps which can be updated". Also includes a header + * as the first element which allows for all items to be updated. + */ +public class MyAppsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { + + private Cursor updatesCursor; + private final Activity activity; + + public MyAppsAdapter(Activity activity) { + this.activity = activity; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = activity.getLayoutInflater(); + switch (viewType) { + case R.id.my_apps__header: + return new UpdatesHeaderController(activity, inflater.inflate(R.layout.my_apps_updates_header, parent, false)); + + case R.id.my_apps__app: + return new AppListItemController(activity, inflater.inflate(R.layout.app_list_item, parent, false)); + + default: + throw new IllegalArgumentException(); + } + } + + @Override + public int getItemCount() { + return updatesCursor == null ? 0 : updatesCursor.getCount() + 1; + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + switch (getItemViewType(position)) { + case R.id.my_apps__header: + ((UpdatesHeaderController) holder).bindModel(updatesCursor.getCount()); + break; + + case R.id.my_apps__app: + updatesCursor.moveToPosition(position - 1); // Subtract one to account for the header. + ((AppListItemController) holder).bindModel(new App(updatesCursor)); + break; + + default: + throw new IllegalArgumentException(); + } + } + + @Override + public int getItemViewType(int position) { + if (position == 0) { + return R.id.my_apps__header; + } else { + return R.id.my_apps__app; + } + } + + public void setApps(Cursor cursor) { + updatesCursor = cursor; + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsViewBinder.java b/app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsViewBinder.java new file mode 100644 index 000000000..8e3ec2c73 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsViewBinder.java @@ -0,0 +1,77 @@ +package org.fdroid.fdroid.views.myapps; + +import android.app.Activity; +import android.database.Cursor; +import android.os.Bundle; +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.view.View; +import android.widget.FrameLayout; + +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.Schema; + +public class MyAppsViewBinder implements LoaderManager.LoaderCallbacks<Cursor> { + + private final MyAppsAdapter adapter; + + private final Activity activity; + + public MyAppsViewBinder(AppCompatActivity activity, FrameLayout parent) { + this.activity = activity; + + View myAppsView = activity.getLayoutInflater().inflate(R.layout.main_tabs, parent, true); + + adapter = new MyAppsAdapter(activity); + + RecyclerView list = (RecyclerView) myAppsView.findViewById(R.id.list); + list.setHasFixedSize(true); + list.setLayoutManager(new LinearLayoutManager(activity)); + list.setAdapter(adapter); + + LoaderManager loaderManager = activity.getSupportLoaderManager(); + loaderManager.initLoader(0, null, this); + } + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + return new CursorLoader( + activity, + AppProvider.getCanUpdateUri(), + new String[]{ + Schema.AppMetadataTable.Cols._ID, // Required for cursor loader to work. + Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME, + Schema.AppMetadataTable.Cols.NAME, + Schema.AppMetadataTable.Cols.SUMMARY, + Schema.AppMetadataTable.Cols.IS_COMPATIBLE, + Schema.AppMetadataTable.Cols.LICENSE, + Schema.AppMetadataTable.Cols.ICON, + Schema.AppMetadataTable.Cols.ICON_URL, + Schema.AppMetadataTable.Cols.InstalledApp.VERSION_CODE, + Schema.AppMetadataTable.Cols.InstalledApp.VERSION_NAME, + Schema.AppMetadataTable.Cols.SuggestedApk.VERSION_NAME, + Schema.AppMetadataTable.Cols.SUGGESTED_VERSION_CODE, + Schema.AppMetadataTable.Cols.REQUIREMENTS, // Needed for filtering apps that require root. + Schema.AppMetadataTable.Cols.ANTI_FEATURES, // Needed for filtering apps that require anti-features. + }, + 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); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/myapps/UpdatesHeaderController.java b/app/src/main/java/org/fdroid/fdroid/views/myapps/UpdatesHeaderController.java new file mode 100644 index 000000000..1bbf61210 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/myapps/UpdatesHeaderController.java @@ -0,0 +1,38 @@ +package org.fdroid.fdroid.views.myapps; + +import android.app.Activity; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.UpdateService; + +public class UpdatesHeaderController extends RecyclerView.ViewHolder { + + private final Activity activity; + private final TextView updatesHeading; + + public UpdatesHeaderController(Activity activity, View itemView) { + super(itemView); + this.activity = activity; + + Button updateAll = (Button) itemView.findViewById(R.id.update_all_button); + updateAll.setOnClickListener(onUpdateAll); + + updatesHeading = (TextView) itemView.findViewById(R.id.updates_heading); + updatesHeading.setText(activity.getString(R.string.updates)); + } + + public void bindModel(int numAppsToUpdate) { + updatesHeading.setText(activity.getResources().getQuantityString(R.plurals.my_apps_header_number_of_updateable, numAppsToUpdate, numAppsToUpdate)); + } + + private final View.OnClickListener onUpdateAll = new View.OnClickListener() { + @Override + public void onClick(View v) { + UpdateService.autoDownloadUpdates(activity); + } + }; +} diff --git a/app/src/main/res/drawable/app_list_item_divider.xml b/app/src/main/res/drawable/app_list_item_divider.xml new file mode 100644 index 000000000..4e5abfa49 --- /dev/null +++ b/app/src/main/res/drawable/app_list_item_divider.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Used to separate two sepparate R.layout.app_list_item views in a list. + As these are not cards, they don't have their own drop shadow or other features that help + separate different list items. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <size android:width="1dp" android:height="1dp" /> + <solid android:color="#ffe3e3e3" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/download_button.xml b/app/src/main/res/drawable/download_button.xml new file mode 100644 index 000000000..4faebbd9f --- /dev/null +++ b/app/src/main/res/drawable/download_button.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Shown in the app list item as a shortcut for the user to be able to download/install an app. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@drawable/ic_download_button" /> +</selector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_download_button.xml b/app/src/main/res/drawable/ic_download_button.xml new file mode 100644 index 000000000..562b5f20e --- /dev/null +++ b/app/src/main/res/drawable/ic_download_button.xml @@ -0,0 +1,13 @@ +<vector android:height="32dp" android:viewportHeight="74.53289" + android:viewportWidth="74.53289" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillAlpha="1" android:fillColor="#0066cc" + android:pathData="m37.27,0c-20.57,0 -37.27,16.7 -37.27,37.27 0,20.57 16.7,37.27 37.27,37.27 20.57,0 37.27,-16.7 37.27,-37.27 0,-20.57 -16.7,-37.27 -37.27,-37.27zM37.27,2c19.49,0 35.27,15.78 35.27,35.27 0,19.49 -15.78,35.27 -35.27,35.27 -19.49,0 -35.27,-15.78 -35.27,-35.27 0,-19.49 15.78,-35.27 35.27,-35.27z" + android:strokeAlpha="1" android:strokeColor="#00000000" + android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="2"/> + <path android:fillAlpha="1" android:fillColor="#0066cc" + android:pathData="M23.05,49.12h27.97v4.04h-27.97z" + android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/> + <path android:fillAlpha="1" android:fillColor="#0066cc" + android:pathData="m31.07,19.19 l0,12.18 -7.71,0 13.86,13.57 13.86,-13.57 -7.83,0 0,-12.18z" + android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/> +</vector> diff --git a/app/src/main/res/layout/app_list_item.xml b/app/src/main/res/layout/app_list_item.xml new file mode 100644 index 000000000..172df3d53 --- /dev/null +++ b/app/src/main/res/layout/app_list_item.xml @@ -0,0 +1,78 @@ +<?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"> + + <ImageView + android:id="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:contentDescription="@string/app_icon" + 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" /> + + <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:lines="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_toStartOf="@+id/install" + android:layout_marginEnd="8dp" + android:layout_marginRight="8dp" /> + + <TextView + android:id="@+id/status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + tools:text="Installed" + 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" /> + + <Button + android:id="@+id/install" + android:background="@drawable/download_button" + android:layout_width="48dp" + android:layout_height="48dp" + android:padding="4dp" + android:layout_marginEnd="16dp" + android:layout_marginRight="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@+id/icon" /> + + <ImageView + android:layout_width="0dp" + android:layout_height="1dp" + android:scaleType="fitXY" + android:src="@drawable/app_list_item_divider" + app:layout_constraintTop_toBottomOf="@+id/status" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="8dp" + tools:ignore="ContentDescription" /> + +</android.support.constraint.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/main_tabs.xml b/app/src/main/res/layout/main_tabs.xml new file mode 100644 index 000000000..d47fb717d --- /dev/null +++ b/app/src/main/res/layout/main_tabs.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + 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.RecyclerView + android:id="@+id/list" + tools:listitem="@layout/app_list_item" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="vertical" + app:layout_constraintTop_toBottomOf="@+id/update_all_button" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" /> + +</LinearLayout> diff --git a/app/src/main/res/layout/my_apps_updates_header.xml b/app/src/main/res/layout/my_apps_updates_header.xml new file mode 100644 index 000000000..f283b406e --- /dev/null +++ b/app/src/main/res/layout/my_apps_updates_header.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="16dp" + android:paddingStart="16dp" + android:paddingRight="16dp" + android:paddingEnd="16dp" + android:layout_marginTop="16dp" + android:paddingBottom="8dp" + android:clipToPadding="false"> + + <Button + android:id="@+id/update_all_button" + android:text="@string/my_apps_btn_update_all" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" /> + + <TextView + android:id="@+id/updates_heading" + tools:text="2 Updates" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_alignBaseline="@+id/update_all_button" /> + +</RelativeLayout> diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 4eaa17c4b..c2fc8ac10 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -7,4 +7,7 @@ <item type="id" name="whats_new_large_tile" /> <item type="id" name="whats_new_small_tile" /> <item type="id" name="whats_new_regular_list" /> + + <item type="id" name="my_apps__header" /> + <item type="id" name="my_apps__app" /> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea8e5e513..4ee0d76f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -63,9 +63,17 @@ <string name="app_not_installed">Not Installed</string> <string name="app_inst_known_source">Installed (from %s)</string> <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="added_on">Added on %s</string> + <string name="my_apps_btn_update_all">Update all</string> + <plurals name="my_apps_header_number_of_updateable"> + <item quantity="one">%1$d Update</item> + <item quantity="other">%1$d Updates</item> + </plurals> + <string name="ok">OK</string> <string name="yes">Yes</string> diff --git a/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java b/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java index 844e81dd9..86c738c78 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java @@ -114,6 +114,9 @@ public class AppProviderTest extends FDroidProviderTest { App notInstalled = AppProvider.Helper.findSpecificApp(r, "not installed", 1, Cols.ALL); assertFalse(notInstalled.canAndWantToUpdate(context)); + assertResultCount(contentResolver, 2, AppProvider.getCanUpdateUri(), PROJ); + assertResultCount(contentResolver, 7, 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); App installedAlreadyLatestIgnoreAll = AppProvider.Helper.findSpecificApp(r, "installed, already latest, ignore all", 1, Cols.ALL); @@ -206,12 +209,14 @@ public class AppProviderTest extends FDroidProviderTest { insertApps(100); assertResultCount(contentResolver, 100, AppProvider.getContentUri(), PROJ); + assertResultCount(contentResolver, 0, AppProvider.getCanUpdateUri(), PROJ); assertResultCount(contentResolver, 0, AppProvider.getInstalledUri(), PROJ); for (int i = 10; i < 20; i++) { InstalledAppTestUtils.install(context, "com.example.test." + i, i, "v1"); } + assertResultCount(contentResolver, 0, AppProvider.getCanUpdateUri(), PROJ); assertResultCount(contentResolver, 10, AppProvider.getInstalledUri(), PROJ); }