My Apps: Added the list of updateable apps to the main view.
Not fully featured yet, because it doesn't listen for broadcasts from the installers, but it is shows the correct list of apps and allows users to queue up downloads of all updateable apps.
This commit is contained in:
parent
69b58c7646
commit
53df5473f5
@ -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) {
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
};
|
||||
}
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
10
app/src/main/res/drawable/app_list_item_divider.xml
Normal file
10
app/src/main/res/drawable/app_list_item_divider.xml
Normal file
@ -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>
|
7
app/src/main/res/drawable/download_button.xml
Normal file
7
app/src/main/res/drawable/download_button.xml
Normal file
@ -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>
|
13
app/src/main/res/drawable/ic_download_button.xml
Normal file
13
app/src/main/res/drawable/ic_download_button.xml
Normal file
@ -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>
|
78
app/src/main/res/layout/app_list_item.xml
Normal file
78
app/src/main/res/layout/app_list_item.xml
Normal file
@ -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>
|
19
app/src/main/res/layout/main_tabs.xml
Normal file
19
app/src/main/res/layout/main_tabs.xml
Normal file
@ -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>
|
32
app/src/main/res/layout/my_apps_updates_header.xml
Normal file
32
app/src/main/res/layout/my_apps_updates_header.xml
Normal file
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user