Merge branch 'new-ui--main-screens--v3' into 'master'
Updates tab + misc UI improvements. Closes #840, #876, #838, and #892 See merge request !444
This commit is contained in:
commit
343e91280a
@ -15,10 +15,13 @@ def getVersionName = { ->
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
maven {
|
||||
url "https://jitpack.io"
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
supportLibVersion = '25.0.1'
|
||||
supportLibVersion = '25.2.0'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -41,12 +44,19 @@ dependencies {
|
||||
compile 'commons-io:commons-io:2.5'
|
||||
compile 'commons-net:commons-net:3.5'
|
||||
compile 'org.openhab.jmdns:jmdns:3.4.2'
|
||||
compile('ch.acra:acra:4.9.1') {
|
||||
exclude module: 'support-v4'
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
compile 'ch.acra:acra:4.9.1'
|
||||
compile 'io.reactivex:rxjava:1.1.0'
|
||||
compile 'io.reactivex:rxandroid:0.23.0'
|
||||
compile 'com.hannesdorfmann:adapterdelegates3:3.0.1'
|
||||
|
||||
// Migrate this to upstream https://github.com/Ashok-Varma/BottomNavigation if PR #110 gets
|
||||
// accepted to drop the minSdk to 10.
|
||||
compile('com.github.pserwylo:BottomNavigation:1.5.0') {
|
||||
// These pull our explicit dependency on 25.2.0 up to 25.3.0 which is broken
|
||||
// (https://code.google.com/p/android/issues/detail?id=251302)
|
||||
exclude module: 'appcompat-v7'
|
||||
exclude module: 'design'
|
||||
}
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
|
||||
@ -103,23 +113,25 @@ if (!hasProperty('sourceDeps')) {
|
||||
'commons-net:commons-net:c25b0da668b3c5649f002d504def22d1b4cb30d206f05428d2fe168fa1a901c2',
|
||||
'com.android.support.constraint:constraint-layout-solver:d03a406eb505dfa673b0087bf17e16d5a4d6bf8afdf452ee175e346207948cdf',
|
||||
'com.android.support.constraint:constraint-layout:df1add69d11063eebba521818d63537b22207376b65f30cc35feea172b84e300',
|
||||
'com.android.support:animated-vector-drawable:70443a2857f9968c4e2c27c107657ce2291d774f8a50f03444e12ab637451175',
|
||||
'com.android.support:appcompat-v7:7fead560a22ea4b15848ce3000f312ef611fac0953bf90ca8710a72a1f6e36ea',
|
||||
'com.android.support:cardview-v7:50d88fae8cd1076cb90504d36ca5ee9df4018555c8f041bd28f43274c0fc9e1f',
|
||||
'com.android.support:design:07a72eb68c888b38d7b78e450e1af8a84e571406e0cf911889e0645d5a41f1e4',
|
||||
'com.android.support:gridlayout-v7:cc11d2a3ee484e078c358a51d23a37e4bfbc542de410cacf275eafc5624bb888',
|
||||
'com.android.support:palette-v7:89700afeedd988b471f0ce528ba916f368f549b47889b86b84d68eee42ea487c',
|
||||
'com.android.support:recyclerview-v7:803baba7be537ace8c5cb8a775e37547c22a04c4b028833796c45c26ec1deca2',
|
||||
'com.android.support:support-annotations:bd94ab42c841db16fb480f4c65d33d297e544655ecc498b37c5cf33a0c5f1968',
|
||||
'com.android.support:support-compat:d04f15aa5f2ae9e8cb7d025bf02dfd4fd6f6800628ceb107e0589634c9e4e537',
|
||||
'com.android.support:support-core-ui:29205ac978a1839d92be3d32db2385dac10f8688bba649e51650023c76de2f00',
|
||||
'com.android.support:support-core-utils:632c3750bd991da8b591f24a8916e74ca6063ae7f525f005c96981725c9bf491',
|
||||
'com.android.support:support-fragment:da47261a1d7c3d33e6e911335a7f4ce01135923bb221d3ab84625d005fa1969f',
|
||||
'com.android.support:support-media-compat:01cac57af687bed9a6cb4ce803bebd1b7e6b8469c14f1f9ac6b4596637ff73d6',
|
||||
'com.android.support:support-v4:50da261acc4ca3d2dea9a43106bf65488711ca97b20a4daa095dba381c205c98',
|
||||
'com.android.support:support-vector-drawable:071ae3695bf8427d3cbfc8791492a3d9c804a4b111aa2a72fbfe7790ea268e5d',
|
||||
'com.android.support:transition:9fd1e6d27cb70b3c5cd19f842b48bbb05cb4e5c93a22372769c342523393e8ea',
|
||||
'com.android.support:animated-vector-drawable:d2d59a11809abe3e64535346f05c22437b458de115f06ea35021fd0714960213',
|
||||
'com.android.support:appcompat-v7:120f3ce6cac682d69e53d80ccfa9cee076f0f11ccbe56d4ccd72099a745e81f9',
|
||||
'com.android.support:cardview-v7:c8610b0c334e4438d7e1ac58fcf2ac891fb26bac662c8351cd6b345c8d7b7076',
|
||||
'com.android.support:design:bf92337c5d0931df50a0dcec81682186dc1fbcf14c2fa1c6d51976963379b64d',
|
||||
'com.android.support:gridlayout-v7:257ac1280f2b3cc3c0afca1cd4d4d2e0b923b92a76b61a9c09fc57e892da7360',
|
||||
'com.android.support:palette-v7:e0050715e0d06fabcc8721b0c2893545fb00be9d761a6ef59ae69101d2368551',
|
||||
'com.android.support:recyclerview-v7:d6ba2c3a6196cc464eb4d69756229523a46eef7804991e5a8cf2d6306dbff10c',
|
||||
'com.android.support:support-annotations:47a2a30eab487a490a8a8f16678007c3d2b6dcae1e09b0485a12bbf921200ec3',
|
||||
'com.android.support:support-compat:5a7b6e18903458e3a561df24033476518f998cd7ae1ed747c2874e0685b999c7',
|
||||
'com.android.support:support-core-ui:cf3c75fd9a1b1dcbb6042d610515cd79cd0d65d3efd272d2250727187e8ca2ed',
|
||||
'com.android.support:support-core-utils:e0561cc9d00ae125d9e1ad8985d4639e68ce8399ae973e91674e97faaf658243',
|
||||
'com.android.support:support-fragment:f12633dd4d418a4edeb5ecf3bf4393edd0770b1eaa5d1ee3078c5e7c174868fd',
|
||||
'com.android.support:support-media-compat:e9f820d08e6a5735cfdb2a7d81d3c86b4a31897ac1edaeb55c7de06bcb370343',
|
||||
'com.android.support:support-v4:cd030f875dc7ee73b58e17598f368a2e12824fb3ceb4ed515ed815a47160228c',
|
||||
'com.android.support:support-vector-drawable:d79752fd68db5a8f5c18125517dafb9e4d7b593c755d188986010e15edd62454',
|
||||
'com.android.support:transition:5a4adefb1b410b23ad62b4477bc612edc47d3dfc8efed488deb8223b70b510d7',
|
||||
'com.github.pserwylo:BottomNavigation:83d7941a7a8d21ba1a8a708cd683b1bb07c6cf898044dc92eadf18a7a7d54f90',
|
||||
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
|
||||
'com.hannesdorfmann:adapterdelegates3:1b20d099d6e7afe57aceca13b713b386959d94a247c3c06a7aeb65b866ece02f',
|
||||
'com.madgag.spongycastle:core:9b6b7ac856b91bcda2ede694eccd26cefb0bf0b09b89f13cda05b5da5ff68c6b',
|
||||
'com.madgag.spongycastle:pkix:6aba9b2210907a3d46dd3dcac782bb3424185290468d102d5207ebdc9796a905',
|
||||
'com.madgag.spongycastle:prov:029f26cd6b67c06ffa05702d426d472c141789001bcb15b7262ed86c868e5643',
|
||||
|
@ -297,8 +297,14 @@
|
||||
<service
|
||||
android:name=".data.InstalledAppProviderService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".AppUpdateStatusService"
|
||||
android:exported="false" />
|
||||
|
||||
<activity android:name=".views.main.MainActivity">
|
||||
<activity
|
||||
android:name=".views.main.MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
@ -35,12 +35,47 @@ import java.util.Map;
|
||||
*/
|
||||
public final class AppUpdateStatusManager {
|
||||
|
||||
static final String BROADCAST_APPSTATUS_LIST_CHANGED = "org.fdroid.fdroid.installer.appstatus.listchange";
|
||||
static final String BROADCAST_APPSTATUS_ADDED = "org.fdroid.fdroid.installer.appstatus.appchange.add";
|
||||
static final String BROADCAST_APPSTATUS_CHANGED = "org.fdroid.fdroid.installer.appstatus.appchange.change";
|
||||
static final String BROADCAST_APPSTATUS_REMOVED = "org.fdroid.fdroid.installer.appstatus.appchange.remove";
|
||||
static final String EXTRA_APK_URL = "urlstring";
|
||||
static final String EXTRA_IS_STATUS_UPDATE = "isstatusupdate";
|
||||
/**
|
||||
* Broadcast when:
|
||||
* * The user clears the list of installed apps from notification manager.
|
||||
* * The user clears the list of apps available to update from the notification manager.
|
||||
* * A repo update is completed and a bunch of new apps are ready to be updated.
|
||||
* * F-Droid is opened, and it finds a bunch of .apk files downloaded and ready to install.
|
||||
*/
|
||||
public static final String BROADCAST_APPSTATUS_LIST_CHANGED = "org.fdroid.fdroid.installer.appstatus.listchange";
|
||||
|
||||
/**
|
||||
* Broadcast when an app begins the download/install process (either manually or via an automatic download).
|
||||
*/
|
||||
public static final String BROADCAST_APPSTATUS_ADDED = "org.fdroid.fdroid.installer.appstatus.appchange.add";
|
||||
|
||||
/**
|
||||
* When the {@link AppUpdateStatus#status} of an app changes or the download progress for an app advances.
|
||||
*/
|
||||
public static final String BROADCAST_APPSTATUS_CHANGED = "org.fdroid.fdroid.installer.appstatus.appchange.change";
|
||||
|
||||
/**
|
||||
* Broadcast when:
|
||||
* * The associated app has the {@link Status#Installed} status, and the user either visits
|
||||
* that apps details page or clears the individual notification for the app.
|
||||
* * The download for an app is cancelled.
|
||||
*/
|
||||
public static final String BROADCAST_APPSTATUS_REMOVED = "org.fdroid.fdroid.installer.appstatus.appchange.remove";
|
||||
|
||||
public static final String EXTRA_APK_URL = "urlstring";
|
||||
|
||||
public static final String EXTRA_REASON_FOR_CHANGE = "reason";
|
||||
|
||||
public static final String REASON_READY_TO_INSTALL = "readytoinstall";
|
||||
public static final String REASON_UPDATES_AVAILABLE = "updatesavailable";
|
||||
public static final String REASON_CLEAR_ALL_UPDATES = "clearallupdates";
|
||||
public static final String REASON_CLEAR_ALL_INSTALLED = "clearallinstalled";
|
||||
|
||||
/**
|
||||
* If this is present and true, then the broadcast has been sent in response to the {@link AppUpdateStatus#status}
|
||||
* changing. In comparison, if it is just the download progress of an app then this should not be true.
|
||||
*/
|
||||
public static final String EXTRA_IS_STATUS_UPDATE = "isstatusupdate";
|
||||
|
||||
private static final String LOGTAG = "AppUpdateStatusManager";
|
||||
|
||||
@ -147,9 +182,11 @@ public final class AppUpdateStatusManager {
|
||||
notifyAdd(entry);
|
||||
}
|
||||
|
||||
private void notifyChange() {
|
||||
private void notifyChange(String reason) {
|
||||
if (!isBatchUpdating) {
|
||||
localBroadcastManager.sendBroadcast(new Intent(BROADCAST_APPSTATUS_LIST_CHANGED));
|
||||
Intent intent = new Intent(BROADCAST_APPSTATUS_LIST_CHANGED);
|
||||
intent.putExtra(EXTRA_REASON_FOR_CHANGE, reason);
|
||||
localBroadcastManager.sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,7 +230,7 @@ public final class AppUpdateStatusManager {
|
||||
for (Apk apk : apksToUpdate) {
|
||||
addApk(apk, status, null);
|
||||
}
|
||||
endBatchUpdates();
|
||||
endBatchUpdates(status);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -291,10 +328,17 @@ public final class AppUpdateStatusManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void endBatchUpdates() {
|
||||
private void endBatchUpdates(Status status) {
|
||||
synchronized (appMapping) {
|
||||
isBatchUpdating = false;
|
||||
notifyChange();
|
||||
|
||||
String reason = null;
|
||||
if (status == Status.ReadyToInstall) {
|
||||
reason = REASON_READY_TO_INSTALL;
|
||||
} else if (status == Status.UpdateAvailable) {
|
||||
reason = REASON_UPDATES_AVAILABLE;
|
||||
}
|
||||
notifyChange(reason);
|
||||
}
|
||||
}
|
||||
|
||||
@ -306,7 +350,7 @@ public final class AppUpdateStatusManager {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
notifyChange();
|
||||
notifyChange(REASON_CLEAR_ALL_UPDATES);
|
||||
}
|
||||
}
|
||||
|
||||
@ -318,7 +362,7 @@ public final class AppUpdateStatusManager {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
notifyChange();
|
||||
notifyChange(REASON_CLEAR_ALL_INSTALLED);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,57 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
import org.fdroid.fdroid.installer.ApkCache;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Scans the list of downloaded .apk files in the cache for each app which can be updated.
|
||||
* If a valid .apk file is found then it will tell the {@link AppUpdateStatusManager} that it is
|
||||
* {@link AppUpdateStatusManager.Status#ReadyToInstall}. This is an {@link IntentService} so as to
|
||||
* run on a background thread, as it hits the disk a bit to figure out the hash of each downloaded
|
||||
* file.
|
||||
*
|
||||
* TODO: Deal with more than just the suggested version. It should also work for people downloading earlier versions (but still newer than their current)
|
||||
* TODO: Identify new apps which have not been installed before, but which have been downloading. Currently only works for updates.
|
||||
*/
|
||||
public class AppUpdateStatusService extends IntentService {
|
||||
|
||||
/**
|
||||
* Queue up a background scan of all downloaded apk files to see if we should notify the user
|
||||
* that they are ready to install.
|
||||
*/
|
||||
public static void scanDownloadedApks(Context context) {
|
||||
context.startService(new Intent(context, AppUpdateStatusService.class));
|
||||
}
|
||||
|
||||
public AppUpdateStatusService() {
|
||||
super("AppUpdateStatusService");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(@Nullable Intent intent) {
|
||||
List<App> apps = AppProvider.Helper.findCanUpdate(this, Schema.AppMetadataTable.Cols.ALL);
|
||||
List<Apk> apksReadyToInstall = new ArrayList<>();
|
||||
for (App app : apps) {
|
||||
Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode);
|
||||
Uri downloadUri = Uri.parse(apk.getUrl());
|
||||
if (ApkCache.apkIsCached(ApkCache.getApkDownloadPath(this, downloadUri), apk)) {
|
||||
apksReadyToInstall.add(apk);
|
||||
}
|
||||
}
|
||||
|
||||
AppUpdateStatusManager.getInstance(this).addApks(apksReadyToInstall, AppUpdateStatusManager.Status.ReadyToInstall);
|
||||
}
|
||||
}
|
@ -234,6 +234,7 @@ public class FDroidApp extends Application {
|
||||
Preferences.get().configureProxy();
|
||||
|
||||
InstalledAppProviderService.compareToPackageManager(this);
|
||||
AppUpdateStatusService.scanDownloadedApks(this);
|
||||
|
||||
// If the user changes the preference to do with filtering rooted apps,
|
||||
// it is easier to just notify a change in the app provider,
|
||||
|
@ -410,7 +410,8 @@ class NotificationHelper {
|
||||
|
||||
// Intent to open main app list
|
||||
Intent intentObject = new Intent(context, MainActivity.class);
|
||||
PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0);
|
||||
intentObject.putExtra(MainActivity.EXTRA_VIEW_UPDATES, true);
|
||||
PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
NotificationCompat.Builder builder =
|
||||
new NotificationCompat.Builder(context)
|
||||
|
@ -20,7 +20,10 @@ import android.support.v7.widget.RecyclerView;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
@ -70,6 +73,18 @@ public class AppListItemController extends RecyclerView.ViewHolder {
|
||||
@Nullable
|
||||
private final TextView ignoredStatus;
|
||||
|
||||
@Nullable
|
||||
private final ProgressBar progressBar;
|
||||
|
||||
@Nullable
|
||||
private final ImageButton cancelButton;
|
||||
|
||||
/**
|
||||
* Will operate as the "Download is complete, click to (install|update)" button.
|
||||
*/
|
||||
@Nullable
|
||||
private final Button actionButton;
|
||||
|
||||
private final DisplayImageOptions displayImageOptions;
|
||||
|
||||
private App currentApp;
|
||||
@ -107,6 +122,17 @@ public class AppListItemController extends RecyclerView.ViewHolder {
|
||||
status = (TextView) itemView.findViewById(R.id.status);
|
||||
installedVersion = (TextView) itemView.findViewById(R.id.installed_version);
|
||||
ignoredStatus = (TextView) itemView.findViewById(R.id.ignored_status);
|
||||
progressBar = (ProgressBar) itemView.findViewById(R.id.progress_bar);
|
||||
cancelButton = (ImageButton) itemView.findViewById(R.id.cancel_button);
|
||||
actionButton = (Button) itemView.findViewById(R.id.action_button);
|
||||
|
||||
if (actionButton != null) {
|
||||
actionButton.setOnClickListener(onInstallClicked);
|
||||
}
|
||||
|
||||
if (cancelButton != null) {
|
||||
cancelButton.setOnClickListener(onCancelDownload);
|
||||
}
|
||||
|
||||
displayImageOptions = Utils.getImageLoadingOptions().build();
|
||||
|
||||
@ -115,7 +141,6 @@ public class AppListItemController extends RecyclerView.ViewHolder {
|
||||
|
||||
public void bindModel(@NonNull App app) {
|
||||
currentApp = app;
|
||||
name.setText(Utils.formatAppNameAndSummary(app.name, app.summary));
|
||||
|
||||
ImageLoader.getInstance().displayImage(app.iconUrl, icon, displayImageOptions);
|
||||
|
||||
@ -129,10 +154,12 @@ public class AppListItemController extends RecyclerView.ViewHolder {
|
||||
broadcastManager.registerReceiver(onDownloadProgress, DownloaderService.getIntentFilter(currentAppDownloadUrl));
|
||||
broadcastManager.registerReceiver(onInstallAction, Installer.getInstallIntentFilter(Uri.parse(currentAppDownloadUrl)));
|
||||
|
||||
configureAppName(app);
|
||||
configureStatusText(app);
|
||||
configureInstalledVersion(app);
|
||||
configureIgnoredStatus(app);
|
||||
configureInstallButton(app);
|
||||
configureActionButton(app);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,8 +167,6 @@ public class AppListItemController extends RecyclerView.ViewHolder {
|
||||
* * 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) {
|
||||
@ -203,6 +228,10 @@ public class AppListItemController extends RecyclerView.ViewHolder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the {@link AppUpdateStatusManager} to find out if there are any apks corresponding to
|
||||
* `app` which are ready to install.
|
||||
*/
|
||||
private boolean isReadyToInstall(@NonNull App app) {
|
||||
for (AppUpdateStatusManager.AppUpdateStatus appStatus : AppUpdateStatusManager.getInstance(activity).getByPackageName(app.packageName)) {
|
||||
if (appStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) {
|
||||
@ -212,13 +241,74 @@ public class AppListItemController extends RecyclerView.ViewHolder {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the {@link AppUpdateStatusManager} to find out if there are any apks corresponding to
|
||||
* `app` which are in the process of being downloaded.
|
||||
*/
|
||||
private boolean isDownloading(@NonNull App app) {
|
||||
for (AppUpdateStatusManager.AppUpdateStatus appStatus : AppUpdateStatusManager.getInstance(activity).getByPackageName(app.packageName)) {
|
||||
if (appStatus.status == AppUpdateStatusManager.Status.Downloading) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The app name {@link TextView} is used for a few reasons:
|
||||
* * Display name + summary of the app (most common).
|
||||
* * If downloading, mention that it is downloading instead of showing the summary.
|
||||
* * If downloaded and ready to install, mention that it is ready to update/install.
|
||||
*/
|
||||
private void configureAppName(@NonNull App app) {
|
||||
if (isReadyToInstall(app)) {
|
||||
if (app.isInstalled()) {
|
||||
String appName = activity.getString(R.string.app_list__name__downloaded_and_ready_to_update, app.name);
|
||||
if (app.lastUpdated != null) {
|
||||
long ageInMillis = System.currentTimeMillis() - app.lastUpdated.getTime();
|
||||
int ageInDays = (int) (ageInMillis / 1000 / 60 / 60 / 24);
|
||||
String age = activity.getResources().getQuantityString(R.plurals.app_list__age__released_x_days_ago, ageInDays, ageInDays);
|
||||
name.setText(appName + "\n" + age);
|
||||
} else {
|
||||
name.setText(appName);
|
||||
}
|
||||
} else {
|
||||
name.setText(activity.getString(R.string.app_list__name__downloaded_and_ready_to_install, app.name));
|
||||
}
|
||||
} else if (isDownloading(app)) {
|
||||
name.setText(activity.getString(R.string.app_list__name__downloading_in_progress, app.name));
|
||||
} else {
|
||||
name.setText(Utils.formatAppNameAndSummary(app.name, app.summary));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The action button will either tell the user to "Update" or "Install" the app. Both actually do
|
||||
* the same thing (launch the package manager). It depends on whether the app has a previous
|
||||
* version installed or not as to the chosen terminology.
|
||||
*/
|
||||
private void configureActionButton(@NonNull App app) {
|
||||
if (actionButton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isReadyToInstall(app)) {
|
||||
actionButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
actionButton.setVisibility(View.VISIBLE);
|
||||
if (app.isInstalled()) {
|
||||
actionButton.setText(R.string.app__install_downloaded_update);
|
||||
} else {
|
||||
actionButton.setText(R.string.menu_install);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
@ -242,6 +332,61 @@ public class AppListItemController extends RecyclerView.ViewHolder {
|
||||
}
|
||||
}
|
||||
|
||||
private void onDownloadStarted() {
|
||||
if (installButton != null) {
|
||||
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_progress));
|
||||
installButton.setImageLevel(0);
|
||||
}
|
||||
|
||||
if (progressBar != null) {
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
progressBar.setIndeterminate(true);
|
||||
}
|
||||
|
||||
if (cancelButton != null) {
|
||||
cancelButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onDownloadProgressUpdated(int bytesRead, int totalBytes) {
|
||||
if (installButton != null) {
|
||||
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_progress));
|
||||
int progressAsDegrees = totalBytes <= 0 ? 0 : (int) (((float) bytesRead / totalBytes) * 360);
|
||||
installButton.setImageLevel(progressAsDegrees);
|
||||
}
|
||||
|
||||
if (progressBar != null) {
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
if (totalBytes <= 0) {
|
||||
progressBar.setIndeterminate(true);
|
||||
} else {
|
||||
progressBar.setIndeterminate(false);
|
||||
progressBar.setMax(totalBytes);
|
||||
progressBar.setProgress(bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelButton != null) {
|
||||
cancelButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onDownloadComplete() {
|
||||
if (installButton != null) {
|
||||
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_complete));
|
||||
}
|
||||
|
||||
if (progressBar != null) {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (cancelButton != null) {
|
||||
cancelButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
configureActionButton(currentApp);
|
||||
}
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final View.OnClickListener onAppClicked = new View.OnClickListener() {
|
||||
@Override
|
||||
@ -262,22 +407,27 @@ public class AppListItemController extends RecyclerView.ViewHolder {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates both the progress bar and the circular install button (which shows progress around the outside of the circle).
|
||||
* Also updates the app label to indicate that the app is being downloaded.
|
||||
*/
|
||||
private final BroadcastReceiver onDownloadProgress = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (installButton == null || currentApp == null || !TextUtils.equals(currentAppDownloadUrl, intent.getDataString())) {
|
||||
if (currentApp == null || !TextUtils.equals(currentAppDownloadUrl, intent.getDataString()) || (installButton == null && progressBar == null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Downloader.ACTION_PROGRESS.equals(intent.getAction())) {
|
||||
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_progress));
|
||||
int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0);
|
||||
int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 100);
|
||||
configureAppName(currentApp);
|
||||
|
||||
int progressAsDegrees = (int) (((float) bytesRead / totalBytes) * 360);
|
||||
installButton.setImageLevel(progressAsDegrees);
|
||||
if (Downloader.ACTION_STARTED.equals(intent.getAction())) {
|
||||
onDownloadStarted();
|
||||
} else if (Downloader.ACTION_PROGRESS.equals(intent.getAction())) {
|
||||
int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0);
|
||||
int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
|
||||
onDownloadProgressUpdated(bytesRead, totalBytes);
|
||||
} else if (Downloader.ACTION_COMPLETE.equals(intent.getAction())) {
|
||||
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_complete));
|
||||
onDownloadComplete();
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -289,6 +439,8 @@ public class AppListItemController extends RecyclerView.ViewHolder {
|
||||
return;
|
||||
}
|
||||
|
||||
configureAppName(currentApp);
|
||||
|
||||
Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
|
||||
if (!TextUtils.equals(apk.packageName, currentApp.packageName)) {
|
||||
return;
|
||||
@ -347,4 +499,16 @@ public class AppListItemController extends RecyclerView.ViewHolder {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final View.OnClickListener onCancelDownload = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (currentAppDownloadUrl == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
InstallManagerService.cancel(activity, currentAppDownloadUrl);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ public class CategorySpan extends ReplacementSpan {
|
||||
canvas.drawRoundRect(iconBackgroundRect, cornerRadius, cornerRadius, iconBackgroundPaint);
|
||||
|
||||
// Category icon on top of the circular background which was just drawn.
|
||||
Drawable icon = ContextCompat.getDrawable(context, R.drawable.ic_category);
|
||||
Drawable icon = ContextCompat.getDrawable(context, R.drawable.ic_categories);
|
||||
icon.setBounds(iconPadding, iconPadding, iconPadding + iconSize, iconPadding + iconSize);
|
||||
icon.draw(canvas);
|
||||
|
||||
|
@ -1,21 +1,31 @@
|
||||
package org.fdroid.fdroid.views.main;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.BottomNavigationView;
|
||||
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.v4.content.LocalBroadcastManager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.text.TextUtils;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
||||
import com.ashokvarma.bottomnavigation.BadgeItem;
|
||||
import com.ashokvarma.bottomnavigation.BottomNavigationBar;
|
||||
import com.ashokvarma.bottomnavigation.BottomNavigationItem;
|
||||
|
||||
import org.fdroid.fdroid.AppDetails;
|
||||
import org.fdroid.fdroid.AppDetails2;
|
||||
import org.fdroid.fdroid.AppUpdateStatusManager;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.NfcHelper;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
@ -23,7 +33,9 @@ import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.UpdateService;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.compat.UriCompat;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.NewRepoConfig;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
import org.fdroid.fdroid.views.ManageReposActivity;
|
||||
import org.fdroid.fdroid.views.apps.AppListActivity;
|
||||
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
|
||||
@ -35,30 +47,38 @@ import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
|
||||
* + Whats new
|
||||
* + Categories list
|
||||
* + App swap
|
||||
* + My apps
|
||||
* + Updates
|
||||
* + Settings
|
||||
*
|
||||
* Users navigate between items by using the bottom navigation bar, or by swiping left and right.
|
||||
* When switching from one screen to the next, we stay within this activity. The new screen will
|
||||
* get inflated (if required)
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity implements BottomNavigationView.OnNavigationItemSelectedListener {
|
||||
public class MainActivity extends AppCompatActivity implements BottomNavigationBar.OnTabSelectedListener,
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
public static final String EXTRA_VIEW_MY_APPS = "org.fdroid.fdroid.views.main.MainActivity.VIEW_MY_APPS";
|
||||
public static final String EXTRA_VIEW_UPDATES = "org.fdroid.fdroid.views.main.MainActivity.VIEW_UPDATES";
|
||||
|
||||
private static final String ADD_REPO_INTENT_HANDLED = "addRepoIntentHandled";
|
||||
|
||||
private static final String ACTION_ADD_REPO = "org.fdroid.fdroid.MainActivity.ACTION_ADD_REPO";
|
||||
|
||||
private static final String STATE_SELECTED_MENU_ID = "selectedMenuId";
|
||||
|
||||
private static final int LOADER_NUM_UPDATES = 1;
|
||||
|
||||
private static final int REQUEST_SWAP = 3;
|
||||
|
||||
private RecyclerView pager;
|
||||
private MainViewAdapter adapter;
|
||||
private BottomNavigationBar bottomNavigation;
|
||||
private int selectedMenuId = R.id.whats_new;
|
||||
private BadgeItem updatesBadge;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
@ -70,8 +90,29 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV
|
||||
pager.setLayoutManager(new NonScrollingHorizontalLayoutManager(this));
|
||||
pager.setAdapter(adapter);
|
||||
|
||||
BottomNavigationView bottomNavigation = (BottomNavigationView) findViewById(R.id.bottom_navigation);
|
||||
bottomNavigation.setOnNavigationItemSelectedListener(this);
|
||||
updatesBadge = new BadgeItem();
|
||||
|
||||
bottomNavigation = (BottomNavigationBar) findViewById(R.id.bottom_navigation);
|
||||
bottomNavigation.setTabSelectedListener(this)
|
||||
.setBarBackgroundColor(R.color.fdroid_blue)
|
||||
.setInActiveColor(R.color.bottom_nav_items)
|
||||
.setActiveColor(android.R.color.white)
|
||||
.setMode(BottomNavigationBar.MODE_FIXED)
|
||||
.addItem(new BottomNavigationItem(R.drawable.ic_latest, R.string.main_menu__latest_apps))
|
||||
.addItem(new BottomNavigationItem(R.drawable.ic_categories, R.string.main_menu__categories))
|
||||
.addItem(new BottomNavigationItem(R.drawable.ic_nearby, R.string.main_menu__swap_nearby))
|
||||
.addItem(new BottomNavigationItem(R.drawable.ic_updates, R.string.updates).setBadgeItem(updatesBadge))
|
||||
.addItem(new BottomNavigationItem(R.drawable.ic_settings, R.string.menu_settings))
|
||||
.initialise();
|
||||
|
||||
IntentFilter updateableAppsFilter = new IntentFilter(AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(onUpdateableAppsChanged, updateableAppsFilter);
|
||||
getSupportLoaderManager().initLoader(LOADER_NUM_UPDATES, null, this);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
selectedMenuId = savedInstanceState.getInt(STATE_SELECTED_MENU_ID, R.id.whats_new);
|
||||
}
|
||||
setSelectedMenuInNav();
|
||||
|
||||
initialRepoUpdateIfRequired();
|
||||
|
||||
@ -79,6 +120,16 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV
|
||||
handleSearchOrAppViewIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
outState.putInt(STATE_SELECTED_MENU_ID, selectedMenuId);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
private void setSelectedMenuInNav() {
|
||||
bottomNavigation.selectTab(adapter.adapterPositionFromItemId(selectedMenuId));
|
||||
}
|
||||
|
||||
/**
|
||||
* The first time the app is run, we will have an empty app list. To deal with this, we will
|
||||
* attempt to update with the default repo. However, if we have tried this at least once, then
|
||||
@ -99,9 +150,11 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV
|
||||
|
||||
FDroidApp.checkStartTor(this);
|
||||
|
||||
if (getIntent().hasExtra(EXTRA_VIEW_MY_APPS)) {
|
||||
getIntent().removeExtra(EXTRA_VIEW_MY_APPS);
|
||||
pager.scrollToPosition(adapter.adapterPositionFromItemId(R.id.my_apps));
|
||||
if (getIntent().hasExtra(EXTRA_VIEW_UPDATES)) {
|
||||
getIntent().removeExtra(EXTRA_VIEW_UPDATES);
|
||||
pager.scrollToPosition(adapter.adapterPositionFromItemId(R.id.updates));
|
||||
selectedMenuId = R.id.updates;
|
||||
setSelectedMenuInNav();
|
||||
}
|
||||
|
||||
// AppDetails 2 and RepoDetailsActivity set different NFC actions, so reset here
|
||||
@ -127,9 +180,19 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
|
||||
pager.scrollToPosition(((MainViewAdapter) pager.getAdapter()).adapterPositionFromItemId(item.getItemId()));
|
||||
return true;
|
||||
public void onTabSelected(int position) {
|
||||
pager.scrollToPosition(position);
|
||||
selectedMenuId = (int) adapter.getItemId(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(int position) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(int position) {
|
||||
|
||||
}
|
||||
|
||||
private void handleSearchOrAppViewIntent(Intent intent) {
|
||||
@ -257,6 +320,36 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
Uri uri = AppProvider.getCanUpdateUri();
|
||||
String[] projection = new String[]{Schema.AppMetadataTable.Cols._COUNT};
|
||||
|
||||
return new CursorLoader(this, uri, projection, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
cursor.moveToFirst();
|
||||
int canUpdateCount = cursor.getInt(cursor.getColumnIndex(Schema.AppMetadataTable.Cols._COUNT));
|
||||
cursor.close();
|
||||
refreshUpdatesBadge(canUpdateCount);
|
||||
}
|
||||
|
||||
private void refreshUpdatesBadge(int canUpdateCount) {
|
||||
if (canUpdateCount == 0) {
|
||||
updatesBadge.hide(true);
|
||||
} else {
|
||||
updatesBadge.setText(Integer.toString(canUpdateCount));
|
||||
updatesBadge.show(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
|
||||
}
|
||||
|
||||
private static class NonScrollingHorizontalLayoutManager extends LinearLayoutManager {
|
||||
NonScrollingHorizontalLayoutManager(Context context) {
|
||||
super(context, LinearLayoutManager.HORIZONTAL, false);
|
||||
@ -273,4 +366,13 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV
|
||||
}
|
||||
}
|
||||
|
||||
private final BroadcastReceiver onUpdateableAppsChanged = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (AppUpdateStatusManager.REASON_UPDATES_AVAILABLE.equals(intent.getStringExtra(AppUpdateStatusManager.EXTRA_REASON_FOR_CHANGE))) {
|
||||
getSupportLoaderManager().restartLoader(LOADER_NUM_UPDATES, null, MainActivity.this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -13,7 +13,7 @@ import org.fdroid.fdroid.R;
|
||||
* + Whats new
|
||||
* + Categories
|
||||
* + Nearby
|
||||
* + My Apps
|
||||
* + Updates
|
||||
* + Settings
|
||||
*
|
||||
* It is responsible for understanding the relationship between each main view that is reachable
|
||||
@ -35,10 +35,26 @@ class MainViewAdapter extends RecyclerView.Adapter<MainViewController> {
|
||||
positionToId.put(0, R.id.whats_new);
|
||||
positionToId.put(1, R.id.categories);
|
||||
positionToId.put(2, R.id.nearby);
|
||||
positionToId.put(3, R.id.my_apps);
|
||||
positionToId.put(3, R.id.updates);
|
||||
positionToId.put(4, R.id.settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(MainViewController holder) {
|
||||
long viewType = getItemId(holder.getAdapterPosition());
|
||||
if (viewType == R.id.updates) {
|
||||
holder.unbindUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(MainViewController holder) {
|
||||
long viewType = getItemId(holder.getAdapterPosition());
|
||||
if (viewType == R.id.updates) {
|
||||
holder.bindUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MainViewController onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
MainViewController holder = createEmptyView();
|
||||
@ -52,8 +68,8 @@ class MainViewAdapter extends RecyclerView.Adapter<MainViewController> {
|
||||
case R.id.nearby:
|
||||
holder.bindSwapView();
|
||||
break;
|
||||
case R.id.my_apps:
|
||||
holder.bindMyApps();
|
||||
case R.id.updates:
|
||||
holder.bindUpdates();
|
||||
break;
|
||||
case R.id.settings:
|
||||
holder.bindSettingsView();
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.fdroid.fdroid.views.main;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
@ -10,7 +11,7 @@ import android.widget.FrameLayout;
|
||||
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.views.fragments.PreferencesFragment;
|
||||
import org.fdroid.fdroid.views.myapps.MyAppsViewBinder;
|
||||
import org.fdroid.fdroid.views.updates.UpdatesViewBinder;
|
||||
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
|
||||
|
||||
/**
|
||||
@ -24,6 +25,9 @@ class MainViewController extends RecyclerView.ViewHolder {
|
||||
private final AppCompatActivity activity;
|
||||
private final FrameLayout frame;
|
||||
|
||||
@Nullable
|
||||
private UpdatesViewBinder updatesView = null;
|
||||
|
||||
MainViewController(AppCompatActivity activity, FrameLayout frame) {
|
||||
super(frame);
|
||||
this.activity = activity;
|
||||
@ -38,10 +42,20 @@ class MainViewController extends RecyclerView.ViewHolder {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see MyAppsViewBinder
|
||||
* @see UpdatesViewBinder
|
||||
*/
|
||||
public void bindMyApps() {
|
||||
new MyAppsViewBinder(activity, frame);
|
||||
public void bindUpdates() {
|
||||
if (updatesView == null) {
|
||||
updatesView = new UpdatesViewBinder(activity, frame);
|
||||
}
|
||||
|
||||
updatesView.bind();
|
||||
}
|
||||
|
||||
public void unbindUpdates() {
|
||||
if (updatesView != null) {
|
||||
updatesView.unbind();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,10 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
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;
|
||||
import org.fdroid.fdroid.views.apps.AppListItemDivider;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
private final AppListItemDivider divider;
|
||||
|
||||
public MyAppsAdapter(Activity activity) {
|
||||
this.activity = activity;
|
||||
divider = new AppListItemDivider(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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView);
|
||||
recyclerView.addItemDecoration(divider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
|
||||
recyclerView.removeItemDecoration(divider);
|
||||
super.onDetachedFromRecyclerView(recyclerView);
|
||||
}
|
||||
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,365 @@
|
||||
package org.fdroid.fdroid.views.updates;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
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.v4.content.LocalBroadcastManager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates3.AdapterDelegatesManager;
|
||||
|
||||
import org.fdroid.fdroid.AppUpdateStatusManager;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
import org.fdroid.fdroid.views.updates.items.AppNotification;
|
||||
import org.fdroid.fdroid.views.updates.items.AppStatus;
|
||||
import org.fdroid.fdroid.views.updates.items.AppUpdateData;
|
||||
import org.fdroid.fdroid.views.updates.items.DonationPrompt;
|
||||
import org.fdroid.fdroid.views.updates.items.UpdateableApp;
|
||||
import org.fdroid.fdroid.views.updates.items.UpdateableAppsHeader;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Manages the following types of information:
|
||||
* * Apps marked for downloading (while the user is offline)
|
||||
* * Currently downloading apps
|
||||
* * Apps which have been downloaded (and need further action to install). This includes new installs and updates.
|
||||
* * Reminders to users that they can donate to apps (only shown infrequently after several updates)
|
||||
* * A list of apps which are eligible to be updated (for when the "Automatic Updates" option is disabled), including:
|
||||
* + A summary of all apps to update including an "Update all" button and a "Show apps" button.
|
||||
* + Once "Show apps" is expanded then each app is shown along with its own download button.
|
||||
*
|
||||
* It does this by maintaining several different lists of interesting apps. Each list contains wrappers
|
||||
* around the piece of data it wants to render ({@link AppStatus}, {@link DonationPrompt},
|
||||
* {@link AppNotification}, {@link UpdateableApp}). Instead of juggling the various viewTypes
|
||||
* to find out which position in the adapter corresponds to which view type, this is handled by
|
||||
* the {@link UpdatesAdapter#delegatesManager}.
|
||||
*
|
||||
* There are a series of type-safe lists which hold the specific data this adapter is interested in.
|
||||
* This data is then collated into a single list (see {@link UpdatesAdapter#populateItems()}) which
|
||||
* is the actual thing the adapter binds too. At any point it is safe to clear the single list and
|
||||
* repopulate it from the original source lists of data. When this is done, the adapter will notify
|
||||
* the recycler view that its data has changed. Sometimes it will also ask the recycler view to
|
||||
* scroll to the newly added item (if attached to the recycler view).
|
||||
*/
|
||||
public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
private final AdapterDelegatesManager<List<AppUpdateData>> delegatesManager = new AdapterDelegatesManager<>();
|
||||
private final List<AppUpdateData> items = new ArrayList<>();
|
||||
|
||||
private final AppCompatActivity activity;
|
||||
|
||||
@Nullable
|
||||
private RecyclerView recyclerView;
|
||||
|
||||
private final List<AppStatus> appsToShowStatus = new ArrayList<>();
|
||||
private final List<DonationPrompt> appsToPromptForDonation = new ArrayList<>();
|
||||
private final List<AppNotification> appsToNotifyAbout = new ArrayList<>();
|
||||
private final List<UpdateableApp> updateableApps = new ArrayList<>();
|
||||
|
||||
private boolean showAllUpdateableApps = false;
|
||||
|
||||
public UpdatesAdapter(AppCompatActivity activity) {
|
||||
this.activity = activity;
|
||||
|
||||
delegatesManager.addDelegate(new AppStatus.Delegate(activity))
|
||||
.addDelegate(new AppNotification.Delegate())
|
||||
.addDelegate(new DonationPrompt.Delegate())
|
||||
.addDelegate(new UpdateableApp.Delegate(activity))
|
||||
.addDelegate(new UpdateableAppsHeader.Delegate(activity));
|
||||
|
||||
populateAppStatuses();
|
||||
notifyDataSetChanged();
|
||||
|
||||
activity.getSupportLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* There are some statuses managed by {@link AppUpdateStatusManager} which we don't care about
|
||||
* for the "Updates" view. For example {@link org.fdroid.fdroid.AppUpdateStatusManager.Status#Installed}
|
||||
* apps are not interesting in the Updates" view at this point in time. Also, although this
|
||||
* adapter does know about apps with updates availble, it does so by querying the database not
|
||||
* by querying the app update status manager. As such, apps with the status
|
||||
* {@link org.fdroid.fdroid.AppUpdateStatusManager.Status#UpdateAvailable} are not interesting here.
|
||||
*/
|
||||
private boolean shouldShowStatus(AppUpdateStatusManager.AppUpdateStatus status) {
|
||||
return status.status == AppUpdateStatusManager.Status.Unknown ||
|
||||
status.status == AppUpdateStatusManager.Status.Downloading ||
|
||||
status.status == AppUpdateStatusManager.Status.ReadyToInstall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds items from the {@link AppUpdateStatusManager} to {@link UpdatesAdapter#appsToShowStatus}.
|
||||
* Note that this will then subsequently rebuild the underlying adapter data structure by
|
||||
* invoking {@link UpdatesAdapter#populateItems}. However as per the populateItems method, it
|
||||
* does not know how best to notify the recycler view of any changes. That is up to the caller
|
||||
* of this method.
|
||||
*/
|
||||
private void populateAppStatuses() {
|
||||
for (AppUpdateStatusManager.AppUpdateStatus status : AppUpdateStatusManager.getInstance(activity).getAll()) {
|
||||
if (shouldShowStatus(status)) {
|
||||
appsToShowStatus.add(new AppStatus(activity, status));
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(appsToShowStatus, new Comparator<AppStatus>() {
|
||||
@Override
|
||||
public int compare(AppStatus o1, AppStatus o2) {
|
||||
return o1.status.app.name.compareTo(o2.status.app.name);
|
||||
}
|
||||
});
|
||||
|
||||
populateItems();
|
||||
}
|
||||
|
||||
public boolean canViewAllUpdateableApps() {
|
||||
return showAllUpdateableApps;
|
||||
}
|
||||
|
||||
public void toggleAllUpdateableApps() {
|
||||
showAllUpdateableApps = !showAllUpdateableApps;
|
||||
populateItems();
|
||||
|
||||
if (showAllUpdateableApps) {
|
||||
notifyItemRangeInserted(appsToShowStatus.size() + 1, updateableApps.size());
|
||||
if (recyclerView != null) {
|
||||
// Scroll so that the "Update X apps" header is at the top of the page, and the
|
||||
// list of apps takes up the rest of the screen.
|
||||
((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(appsToShowStatus.size(), 0);
|
||||
}
|
||||
} else {
|
||||
notifyItemRangeRemoved(appsToShowStatus.size() + 1, updateableApps.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely rebuilds the underlying data structure used by this adapter. Note however, that
|
||||
* this does not notify the recycler view of any changes. Thus, it is up to other methods which
|
||||
* initiate a call to this method to make sure they appropriately notify the recyler view.
|
||||
*/
|
||||
private void populateItems() {
|
||||
items.clear();
|
||||
|
||||
items.addAll(appsToShowStatus);
|
||||
|
||||
if (updateableApps != null && updateableApps.size() > 0) {
|
||||
items.add(new UpdateableAppsHeader(activity, this, updateableApps));
|
||||
if (showAllUpdateableApps) {
|
||||
items.addAll(updateableApps);
|
||||
}
|
||||
}
|
||||
|
||||
items.addAll(appsToPromptForDonation);
|
||||
items.addAll(appsToNotifyAbout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return delegatesManager.getItemViewType(items, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
return delegatesManager.onCreateViewHolder(parent, viewType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
delegatesManager.onBindViewHolder(items, position, holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView);
|
||||
this.recyclerView = recyclerView;
|
||||
}
|
||||
|
||||
@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,
|
||||
Schema.AppMetadataTable.Cols.NAME
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
int numberRemoved = updateableApps.size();
|
||||
boolean hadHeader = updateableApps.size() > 0;
|
||||
boolean willHaveHeader = cursor.getCount() > 0;
|
||||
|
||||
updateableApps.clear();
|
||||
notifyItemRangeRemoved(appsToShowStatus.size(), numberRemoved + (hadHeader ? 1 : 0));
|
||||
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
updateableApps.add(new UpdateableApp(activity, new App(cursor)));
|
||||
cursor.moveToNext();
|
||||
}
|
||||
|
||||
populateItems();
|
||||
notifyItemRangeInserted(appsToShowStatus.size(), updateableApps.size() + (willHaveHeader ? 1 : 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) { }
|
||||
|
||||
/**
|
||||
* Doesn't listen for {@link AppUpdateStatusManager#BROADCAST_APPSTATUS_CHANGED} because the
|
||||
* individual items in the recycler view will listen for the appropriate changes in state and
|
||||
* update themselves accordingly (if they are displayed).
|
||||
*/
|
||||
public void listenForStatusUpdates() {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED);
|
||||
filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED);
|
||||
filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED);
|
||||
|
||||
LocalBroadcastManager.getInstance(activity).registerReceiver(receiverAppStatusChanges, filter);
|
||||
}
|
||||
|
||||
public void stopListeningForStatusUpdates() {
|
||||
LocalBroadcastManager.getInstance(activity).unregisterReceiver(receiverAppStatusChanges);
|
||||
}
|
||||
|
||||
private void onManyAppStatusesChanged(String reasonForChange) {
|
||||
switch (reasonForChange) {
|
||||
case AppUpdateStatusManager.REASON_UPDATES_AVAILABLE:
|
||||
onUpdateableAppsChanged();
|
||||
break;
|
||||
|
||||
case AppUpdateStatusManager.REASON_READY_TO_INSTALL:
|
||||
onFoundAppsReadyToInstall();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apps have been made available for update which were not available for update before.
|
||||
* We need to rerun our database query to get a list of apps to update.
|
||||
*/
|
||||
private void onUpdateableAppsChanged() {
|
||||
activity.getSupportLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* We have completed a scan of .apk files in the cache, and identified there are
|
||||
* some which are ready to install.
|
||||
*/
|
||||
private void onFoundAppsReadyToInstall() {
|
||||
if (appsToShowStatus.size() > 0) {
|
||||
int size = appsToShowStatus.size();
|
||||
appsToShowStatus.clear();
|
||||
notifyItemRangeRemoved(0, size);
|
||||
}
|
||||
|
||||
populateAppStatuses();
|
||||
notifyItemRangeInserted(0, appsToShowStatus.size());
|
||||
|
||||
if (recyclerView != null) {
|
||||
recyclerView.smoothScrollToPosition(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAppStatusAdded(String apkUrl) {
|
||||
// We could try and find the specific place where we need to add our new item, but it is
|
||||
// far simpler to clear the list and rebuild it (sorting it in the process).
|
||||
appsToShowStatus.clear();
|
||||
populateAppStatuses();
|
||||
|
||||
// After adding the new item to our list (somewhere) we can then look it back up again in
|
||||
// order to notify the recycler view and scroll to that item.
|
||||
int positionOfNewApp = 0;
|
||||
for (int i = 0; i < appsToShowStatus.size(); i++) {
|
||||
if (TextUtils.equals(appsToShowStatus.get(i).status.getUniqueKey(), apkUrl)) {
|
||||
positionOfNewApp = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
notifyItemInserted(positionOfNewApp);
|
||||
|
||||
if (recyclerView != null) {
|
||||
recyclerView.smoothScrollToPosition(positionOfNewApp);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAppStatusRemoved(String apkUrl) {
|
||||
// Find out where the item is in our internal data structure, so that we can remove it and
|
||||
// also notify the recycler view appropriately.
|
||||
int positionOfOldApp = 0;
|
||||
for (int i = 0; i < appsToShowStatus.size(); i++) {
|
||||
if (TextUtils.equals(appsToShowStatus.get(i).status.getUniqueKey(), apkUrl)) {
|
||||
positionOfOldApp = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
appsToShowStatus.remove(positionOfOldApp);
|
||||
|
||||
populateItems();
|
||||
notifyItemRemoved(positionOfOldApp);
|
||||
}
|
||||
|
||||
private final BroadcastReceiver receiverAppStatusChanges = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String apkUrl = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL);
|
||||
|
||||
switch (intent.getAction()) {
|
||||
case AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED:
|
||||
onManyAppStatusesChanged(intent.getStringExtra(AppUpdateStatusManager.EXTRA_REASON_FOR_CHANGE));
|
||||
break;
|
||||
|
||||
case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED:
|
||||
onAppStatusAdded(apkUrl);
|
||||
break;
|
||||
|
||||
case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED:
|
||||
onAppStatusRemoved(apkUrl);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.fdroid.fdroid.views.updates;
|
||||
|
||||
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;
|
||||
|
||||
public class UpdatesViewBinder {
|
||||
|
||||
private final UpdatesAdapter adapter;
|
||||
|
||||
public UpdatesViewBinder(AppCompatActivity activity, FrameLayout parent) {
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.main_tab_updates, parent, true);
|
||||
|
||||
adapter = new UpdatesAdapter(activity);
|
||||
|
||||
RecyclerView list = (RecyclerView) view.findViewById(R.id.list);
|
||||
list.setHasFixedSize(true);
|
||||
list.setLayoutManager(new LinearLayoutManager(activity));
|
||||
list.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public void bind() {
|
||||
adapter.listenForStatusUpdates();
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
adapter.stopListeningForStatusUpdates();
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package org.fdroid.fdroid.views.updates.items;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Each of these apps has a notification to display to the user.
|
||||
* The notification will have come from the apps metadata, provided by its maintainer. It may be
|
||||
* something about the app being removed from the repository, or perhaps security problems that
|
||||
* were identified in the app.
|
||||
*/
|
||||
public class AppNotification extends AppUpdateData {
|
||||
|
||||
public AppNotification(Activity activity) {
|
||||
super(activity);
|
||||
}
|
||||
|
||||
public static class Delegate extends AdapterDelegate<List<AppUpdateData>> {
|
||||
|
||||
@Override
|
||||
protected boolean isForViewType(@NonNull List<AppUpdateData> items, int position) {
|
||||
return items.get(position) instanceof AppNotification;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
|
||||
return new ViewHolder(new TextView(parent.getContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
|
||||
AppNotification app = (AppNotification) items.get(position);
|
||||
((ViewHolder) holder).bindApp(app);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
public void bindApp(AppNotification app) {
|
||||
((TextView) itemView).setText("Notification for app");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package org.fdroid.fdroid.views.updates.items;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate;
|
||||
|
||||
import org.fdroid.fdroid.AppUpdateStatusManager;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.views.apps.AppListItemController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Apps which we want to show some more substantial information about.
|
||||
* @see R.layout#updateable_app_status_item The view that this binds to
|
||||
* @see AppListItemController Used for binding the {@link App} to the {@link R.layout#updateable_app_status_item}
|
||||
*/
|
||||
public class AppStatus extends AppUpdateData {
|
||||
|
||||
public final AppUpdateStatusManager.AppUpdateStatus status;
|
||||
|
||||
public AppStatus(Activity activity, AppUpdateStatusManager.AppUpdateStatus status) {
|
||||
super(activity);
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public static class Delegate extends AdapterDelegate<List<AppUpdateData>> {
|
||||
|
||||
private final Activity activity;
|
||||
|
||||
public Delegate(Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isForViewType(@NonNull List<AppUpdateData> items, int position) {
|
||||
return items.get(position) instanceof AppStatus;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
|
||||
return new AppListItemController(activity, activity.getLayoutInflater().inflate(R.layout.updateable_app_status_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
|
||||
AppStatus app = (AppStatus) items.get(position);
|
||||
((AppListItemController) holder).bindModel(app.status.app);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.fdroid.fdroid.views.updates.items;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
/**
|
||||
* Used as a common base class for all data types in the {@link org.fdroid.fdroid.views.updates.UpdatesAdapter}.
|
||||
* Doesn't have any functionality of its own, but allows the {@link org.fdroid.fdroid.views.updates.UpdatesAdapter#delegatesManager}
|
||||
* to specify a data type more specific than just {@link Object}.
|
||||
*/
|
||||
public abstract class AppUpdateData {
|
||||
public final Activity activity;
|
||||
|
||||
public AppUpdateData(Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package org.fdroid.fdroid.views.updates.items;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The app (if any) which we should prompt the user about potentially donating to (due to having
|
||||
* updated several times).
|
||||
*/
|
||||
public class DonationPrompt extends AppUpdateData {
|
||||
|
||||
public DonationPrompt(Activity activity) {
|
||||
super(activity);
|
||||
}
|
||||
|
||||
public static class Delegate extends AdapterDelegate<List<AppUpdateData>> {
|
||||
|
||||
@Override
|
||||
protected boolean isForViewType(@NonNull List<AppUpdateData> items, int position) {
|
||||
return items.get(position) instanceof DonationPrompt;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
|
||||
return new ViewHolder(new TextView(parent.getContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
|
||||
DonationPrompt app = (DonationPrompt) items.get(position);
|
||||
((ViewHolder) holder).bindApp(app);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
public void bindApp(DonationPrompt app) {
|
||||
((TextView) itemView).setText("Donation prompt for app");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package org.fdroid.fdroid.views.updates.items;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate;
|
||||
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.views.apps.AppListItemController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* List of all apps which can be updated, but have not yet been downloaded.
|
||||
* @see UpdateableApp The data that is bound to this view.
|
||||
* @see R.layout#updateable_app_list_item The view that this binds to.
|
||||
* @see AppListItemController Used for binding the {@link App} to the {@link R.layout#updateable_app_list_item}
|
||||
*/
|
||||
public class UpdateableApp extends AppUpdateData {
|
||||
|
||||
public final App app;
|
||||
|
||||
public UpdateableApp(Activity activity, App app) {
|
||||
super(activity);
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
public static class Delegate extends AdapterDelegate<List<AppUpdateData>> {
|
||||
|
||||
private final Activity activity;
|
||||
|
||||
public Delegate(Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isForViewType(@NonNull List<AppUpdateData> items, int position) {
|
||||
return items.get(position) instanceof UpdateableApp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
|
||||
return new AppListItemController(activity, activity.getLayoutInflater().inflate(R.layout.updateable_app_list_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
|
||||
UpdateableApp app = (UpdateableApp) items.get(position);
|
||||
((AppListItemController) holder).bindModel(app.app);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
package org.fdroid.fdroid.views.updates.items;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate;
|
||||
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.UpdateService;
|
||||
import org.fdroid.fdroid.views.updates.UpdatesAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Summary of all apps that can be downloaded. Includes a button to download all of them and also
|
||||
* a toggle to show or hide the list of each individual item.
|
||||
* @see R.layout#updates_header The view that this binds to.
|
||||
* @see UpdateableAppsHeader The data that is bound to this view.
|
||||
*/
|
||||
public class UpdateableAppsHeader extends AppUpdateData {
|
||||
|
||||
public final List<UpdateableApp> apps;
|
||||
public final UpdatesAdapter adapter;
|
||||
|
||||
public UpdateableAppsHeader(Activity activity, UpdatesAdapter updatesAdapter, List<UpdateableApp> updateableApps) {
|
||||
super(activity);
|
||||
apps = updateableApps;
|
||||
adapter = updatesAdapter;
|
||||
}
|
||||
|
||||
public static class Delegate extends AdapterDelegate<List<AppUpdateData>> {
|
||||
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
public Delegate(Activity activity) {
|
||||
inflater = activity.getLayoutInflater();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isForViewType(@NonNull List<AppUpdateData> items, int position) {
|
||||
return items.get(position) instanceof UpdateableAppsHeader;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
|
||||
return new ViewHolder(inflater.inflate(R.layout.updates_header, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
|
||||
UpdateableAppsHeader app = (UpdateableAppsHeader) items.get(position);
|
||||
((ViewHolder) holder).bindHeader(app);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
|
||||
private UpdateableAppsHeader header;
|
||||
|
||||
private final TextView updatesAvailable;
|
||||
private final ImageView downloadAll;
|
||||
private final TextView appsToUpdate;
|
||||
private final Button toggleAppsToUpdate;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
updatesAvailable = (TextView) itemView.findViewById(R.id.text_updates_available);
|
||||
downloadAll = (ImageView) itemView.findViewById(R.id.button_download_all);
|
||||
appsToUpdate = (TextView) itemView.findViewById(R.id.text_apps_to_update);
|
||||
toggleAppsToUpdate = (Button) itemView.findViewById(R.id.button_toggle_apps_to_update);
|
||||
|
||||
toggleAppsToUpdate.setOnClickListener(this);
|
||||
downloadAll.setOnClickListener(this);
|
||||
}
|
||||
|
||||
public void bindHeader(UpdateableAppsHeader header) {
|
||||
this.header = header;
|
||||
|
||||
updatesAvailable.setText(itemView.getResources().getQuantityString(R.plurals.updates__download_updates_for_apps, header.apps.size(), header.apps.size()));
|
||||
|
||||
List<String> appNames = new ArrayList<>(header.apps.size());
|
||||
for (UpdateableApp app : header.apps) {
|
||||
appNames.add(app.app.name);
|
||||
}
|
||||
|
||||
appsToUpdate.setText(TextUtils.join(", ", appNames));
|
||||
updateToggleButtonText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v == toggleAppsToUpdate) {
|
||||
header.adapter.toggleAllUpdateableApps();
|
||||
updateToggleButtonText();
|
||||
} else if (v == downloadAll) {
|
||||
UpdateService.autoDownloadUpdates(header.activity);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateToggleButtonText() {
|
||||
if (header.adapter.canViewAllUpdateableApps()) {
|
||||
toggleAppsToUpdate.setText(R.string.updates__hide_updateable_apps);
|
||||
} else {
|
||||
toggleAppsToUpdate.setText(R.string.updates__show_updateable_apps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
6
app/src/main/res/color/bottom_nav_items.xml
Normal file
6
app/src/main/res/color/bottom_nav_items.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@android:color/white" android:state_checked="true"/>
|
||||
<item android:color="@android:color/white" android:state_pressed="true"/>
|
||||
<item android:color="#B8D4F0"/>
|
||||
</selector>
|
9
app/src/main/res/drawable/badge_background.xml
Normal file
9
app/src/main/res/drawable/badge_background.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="@dimen/badge_size" />
|
||||
<solid android:color="#ffdd0000" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
@ -2,25 +2,25 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="3dp" />
|
||||
<corners android:radius="24dp" />
|
||||
<solid android:color="@color/fdroid_blue_dark" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_checked="true">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="3dp" />
|
||||
<corners android:radius="24dp" />
|
||||
<solid android:color="@color/fdroid_blue_dark" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_enabled="false">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="3dp" />
|
||||
<corners android:radius="24dp" />
|
||||
<solid android:color="@color/fdroid_blue_dark" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="3dp" />
|
||||
<corners android:radius="24dp" />
|
||||
<solid android:color="@color/fdroid_blue" />
|
||||
</shape>
|
||||
</item>
|
||||
|
@ -2,25 +2,25 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="3dp" />
|
||||
<corners android:radius="24dp" />
|
||||
<solid android:color="@color/fdroid_blue" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_checked="true">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="3dp" />
|
||||
<corners android:radius="24dp" />
|
||||
<solid android:color="@color/fdroid_blue" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_enabled="false">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="3dp" />
|
||||
<corners android:radius="24dp" />
|
||||
<solid android:color="@color/fdroid_blue" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="3dp" />
|
||||
<corners android:radius="24dp" />
|
||||
<solid android:color="@android:color/white" />
|
||||
<stroke android:color="@color/fdroid_blue" android:width="2dp" />
|
||||
</shape>
|
||||
|
6
app/src/main/res/drawable/ic_categories.xml
Normal file
6
app/src/main/res/drawable/ic_categories.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<vector android:height="24dp" android:viewportHeight="42.0"
|
||||
android:viewportWidth="42.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="M38,3H4C2.9,3 2,3.9 2,5v12c0,1.1 0.9,2 2,2h34c1.1,0 2,-0.9 2,-2V5C40,3.9 39.1,3 38,3m0,20H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h34c1.1,0 2,-0.9 2,-2V25c0,-1.1 -0.9,-2 -2,-2"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_latest.xml
Normal file
9
app/src/main/res/drawable/ic_latest.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector android:height="24dp" android:viewportHeight="47.0"
|
||||
android:viewportWidth="47.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="M23.9,10.2l-11.4,1.64l8.25,7.94l-1.95,11.22l10.2,-5.3l10.2,5.3l-1.95,-11.22l8.25,-7.94l-11.4,-1.64l-5.1,-10.2z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m17.07,35.44c-0.58,0 -1.15,-0.18 -1.64,-0.53 -0.86,-0.62 -1.29,-1.68 -1.11,-2.72L16.06,22.07 15.04,21.09L4.21,21.09c-1.5,0 -2.71,1.21 -2.71,2.71L1.5,44.29c0,1.5 1.21,2.71 2.71,2.71L24.7,47c1.5,0 2.71,-1.21 2.71,-2.71L27.41,30.36l-9.05,4.76c-0.41,0.21 -0.85,0.32 -1.3,0.32"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
@ -1,6 +0,0 @@
|
||||
<vector android:height="24dp" android:viewportHeight="64.0"
|
||||
android:viewportWidth="64.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="1" android:fillColor="#ffffff"
|
||||
android:pathData="m32,0a32,32 0,0 0,-32 32,32 32,0 0,0 32,32 32,32 0,0 0,32 -32,32 32,0 0,0 -32,-32zM32.56,10.63a9.46,9.46 0,0 1,9.46 9.46,9.46 9.46,0 0,1 -9.46,9.46 9.46,9.46 0,0 1,-9.46 -9.46,9.46 9.46,0 0,1 9.46,-9.46zM32,36.45a33.39,33.39 0,0 1,20.37 6.98,23.37 23.37,0 0,1 -20.37,11.94 23.37,23.37 0,0 1,-20.37 -11.96,33.39 33.39,0 0,1 20.37,-6.96z"
|
||||
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
|
||||
</vector>
|
@ -1,12 +1,18 @@
|
||||
<vector android:height="24dp" android:viewportHeight="64.0"
|
||||
android:viewportWidth="64.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<vector android:height="24dp" android:viewportHeight="48.0"
|
||||
android:viewportWidth="48.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m22.75,12.78c-6.19,0 -11.22,5.03 -11.22,11.22 0,0.55 0.05,1.08 0.13,1.61 1.71,0.27 3.29,0.93 4.64,1.91 -0.57,-1.05 -0.9,-2.24 -0.9,-3.52 0,-4.05 3.3,-7.35 7.35,-7.35 4.05,0 7.35,3.3 7.35,7.35 0,4.05 -3.3,7.35 -7.35,7.35 -1.38,0 -2.66,-0.39 -3.77,-1.05 0.92,1.39 1.53,2.99 1.73,4.73 0.66,0.12 1.34,0.2 2.04,0.2 6.19,0 11.22,-5.03 11.22,-11.22 0,-6.19 -5.03,-11.22 -11.22,-11.22"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m42.28,25.4c-0.36,0 -0.71,-0.02 -1.06,-0.06 -0.69,9.6 -8.71,17.2 -18.48,17.2 -1.28,0 -2.53,-0.13 -3.74,-0.38 -0.83,1.26 -1.91,2.33 -3.18,3.15 2.18,0.71 4.5,1.1 6.91,1.1 12.03,0 21.87,-9.54 22.38,-21.45 -0.9,0.29 -1.85,0.44 -2.84,0.44"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m42.28,10.64c-2.96,0 -5.36,2.41 -5.36,5.36 0,2.96 2.41,5.36 5.36,5.36 2.96,0 5.36,-2.41 5.36,-5.36 0,-2.96 -2.41,-5.36 -5.36,-5.36"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m10,29.5c-3.72,0 -6.75,3.03 -6.75,6.75 0,3.72 3.03,6.75 6.75,6.75 3.72,0 6.75,-3.03 6.75,-6.75 0,-3.72 -3.03,-6.75 -6.75,-6.75"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#ffffff"
|
||||
android:pathData="m31.52,0.48a31.52,31.52 45.69,0 0,-31.52 31.52,31.52 31.52,45.64 0,0 0.45,5.11 16.53,16.53 53.72,0 1,4.52 -2.34,26.71 26.71,48.01 0,1 -0.18,-2.78 26.71,26.71 49.01,0 1,26.72 -26.71,26.71 26.71,48.46 0,1 23.41,13.91 14.8,14.8 67.3,0 1,2.73 -0.26,14.8 14.8,67.32 0,1 2.62,0.24 31.52,31.52 0,0 0,-28.76 -18.69zM31.52,14.32a17.68,17.68 99.47,0 0,-17.68 17.68,17.68 17.68,99.47 0,0 0.18,2.39 16.53,16.53 53.72,0 1,12.62 14.6,17.68 17.68,99.47 0,0 4.88,0.69 17.68,17.68 99.2,0 0,14.22 -7.21,14.8 14.8,66.35 0,1 -2.88,-8.75 14.8,14.8 67.3,0 1,4.14 -10.27,17.68 17.68,99.2 0,0 -15.48,-9.15zM31.52,21.62a10.38,10.38 75.21,0 1,10.38 10.38,10.38 10.38,75.3 0,1 -10.38,10.38 10.38,10.38 76.55,0 1,-10.38 -10.38,10.38 10.38,76.21 0,1 10.38,-10.38zM53.04,47.77a26.71,26.71 48.42,0 1,-21.52 10.94,26.71 26.71,48.96 0,1 -6.56,-0.85 16.53,16.53 54.54,0 1,-2.96 4.15,31.52 31.52,45.67 0,0 9.52,1.51 31.52,31.52 47.06,0 0,26.83 -15.03,14.8 14.8,67.3 0,1 -0.69,0.04 14.8,14.8 67.32,0 1,-4.61 -0.76z"
|
||||
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#ffffff"
|
||||
android:pathData="M10.19,50.45m-7.88,0a7.88,7.88 53.72,1 1,15.76 0a7.88,7.88 53.29,1 1,-15.76 0"
|
||||
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#ffffff"
|
||||
android:pathData="M57.66,33.73m-6.34,0a6.34,6.34 114.83,1 1,12.68 0a6.34,6.34 114.83,1 1,-12.68 0"
|
||||
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
|
||||
android:pathData="m4.47,27.01c-0.16,-0.98 -0.25,-1.98 -0.25,-3.01 0,-10.22 8.31,-18.53 18.53,-18.53 4.68,0 8.97,1.75 12.23,4.63C35.81,9.08 36.85,8.24 38.03,7.64 34.02,3.9 28.65,1.6 22.75,1.6 10.4,1.6 0.35,11.65 0.35,24c0,2.1 0.3,4.13 0.84,6.05 0.87,-1.23 1.99,-2.27 3.28,-3.04"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
||||
|
@ -1,6 +0,0 @@
|
||||
<vector android:height="24dp" android:viewportHeight="64.0"
|
||||
android:viewportWidth="64.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="1" android:fillColor="#ffffff"
|
||||
android:pathData="m40.15,0.71 l-9.57,5.97 -10.14,-4.94 -4.23,10.45 -11.11,1.96 2.72,10.95 -7.83,8.11 8.63,7.25 -1.56,11.17 11.25,0.79 5.3,9.95 9.57,-5.97 10.14,4.94 4.23,-10.45 11.11,-1.96 -2.72,-10.94 7.83,-8.12 -8.63,-7.25 1.56,-11.17 -11.25,-0.79 -5.3,-9.95zM28.45,15.95 L34.06,15.95 34.06,32.52 28.45,32.52 28.45,15.95zM28.45,38 L34.06,38 34.06,43.6 28.45,43.6 28.45,38z"
|
||||
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
|
||||
</vector>
|
@ -1,6 +1,6 @@
|
||||
<vector android:height="24dp" android:viewportHeight="64.0"
|
||||
android:viewportWidth="64.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="1" android:fillColor="#ffffff"
|
||||
android:pathData="m23.4,0.01 l0,8.54c0.02,1.79 -5.02,5.64 -7.4,4.27l-7.4,-4.27 -8.6,14.9 7.4,4.27c2.65,1.48 2.93,6.85 0,8.54l-7.4,4.27 8.6,14.9 7.4,-4.27c2.29,-1.36 7.4,0.81 7.4,4.27l0,8.55 17.2,0 0,-8.55c0,-3.23 4.16,-6.14 7.4,-4.27l7.4,4.27 8.6,-14.9 -7.4,-4.27c-2.47,-1.43 -2.98,-6.82 0,-8.54l7.4,-4.27 -8.6,-14.9 -7.4,4.27c-2.16,1.29 -7.4,-0.67 -7.4,-4.27l0,-8.54 -17.2,0zM32.14,20.4a11.76,11.76 0,0 1,11.76 11.76,11.76 11.76,0 0,1 -11.76,11.76 11.76,11.76 0,0 1,-11.75 -11.76,11.76 11.76,0 0,1 11.75,-11.76z"
|
||||
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
|
||||
<vector android:height="24dp" android:viewportHeight="40.0"
|
||||
android:viewportWidth="39.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m34.32,21.96c0.08,-0.64 0.14,-1.28 0.14,-1.96 0,-0.68 -0.06,-1.32 -0.14,-1.96l4.22,-3.3c0.38,-0.3 0.48,-0.84 0.24,-1.28l-4,-6.92c-0.24,-0.44 -0.78,-0.6 -1.22,-0.44l-4.98,2c-1.04,-0.8 -2.16,-1.46 -3.38,-1.96l-0.76,-5.3c-0.06,-0.48 -0.48,-0.84 -0.98,-0.84h-8c-0.5,0 -0.92,0.36 -0.98,0.84l-0.76,5.3c-1.22,0.5 -2.34,1.18 -3.38,1.96l-4.98,-2C4.9,5.92 4.38,6.1 4.14,6.54l-4,6.92c-0.26,0.44 -0.14,0.98 0.24,1.28l4.22,3.3c-0.08,0.64 -0.14,1.3 -0.14,1.96 0,0.66 0.06,1.32 0.14,1.96l-4.22,3.3c-0.38,0.3 -0.48,0.84 -0.24,1.28l4,6.92c0.24,0.44 0.78,0.6 1.22,0.44l4.98,-2c1.04,0.8 2.16,1.46 3.38,1.96l0.76,5.3c0.06,0.48 0.48,0.84 0.98,0.84h8c0.5,0 0.92,-0.36 0.98,-0.84l0.76,-5.3c1.22,-0.5 2.34,-1.18 3.38,-1.96l4.98,2c0.46,0.18 0.98,0 1.22,-0.44l4,-6.92c0.24,-0.44 0.14,-0.98 -0.24,-1.28zM19.46,27c-3.86,0 -7,-3.14 -7,-7 0,-3.86 3.14,-7 7,-7 3.86,0 7,3.14 7,7 0,3.86 -3.14,7 -7,7z"
|
||||
/>
|
||||
</vector>
|
||||
|
6
app/src/main/res/drawable/ic_updates.xml
Normal file
6
app/src/main/res/drawable/ic_updates.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<vector android:height="24dp" android:viewportHeight="42.0"
|
||||
android:viewportWidth="42.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m21,42c2.37,0 4.31,-1.94 4.31,-4.31H16.69C16.69,40.06 18.61,42 21,42ZM33.92,29.08V18.31C33.92,11.7 30.39,6.16 24.23,4.7V3.23C24.23,1.44 22.79,0 21,0 19.21,0 17.77,1.44 17.77,3.23V4.7C11.59,6.16 8.08,11.67 8.08,18.31v10.77l-4.31,4.31v2.15H38.23v-2.15z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
@ -6,22 +6,19 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<android.support.design.widget.BottomNavigationView
|
||||
<com.ashokvarma.bottomnavigation.BottomNavigationBar
|
||||
android:id="@+id/bottom_navigation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
app:menu="@menu/main_activity_screens"
|
||||
android:background="@color/fdroid_blue"
|
||||
app:itemBackground="@color/fdroid_blue"
|
||||
app:itemIconTint="@android:color/white"
|
||||
app:itemTextColor="@android:color/white" />
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/main_view_pager"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_above="@+id/bottom_navigation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/main_tab_whats_new" />
|
||||
|
||||
</RelativeLayout>
|
@ -114,6 +114,7 @@
|
||||
android:layout_toRightOf="@id/icon"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:clipToPadding="false"
|
||||
android:visibility="gone"
|
||||
>
|
||||
|
||||
@ -125,7 +126,8 @@
|
||||
android:layout_weight="1"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="marquee"
|
||||
android:text="THIS IS BUTTON 1" />
|
||||
android:padding="12dp"
|
||||
tools:text="THIS IS BUTTON 1" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/primaryButtonView"
|
||||
@ -137,7 +139,8 @@
|
||||
android:layout_weight="1"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="marquee"
|
||||
android:text="THIS IS 2" />
|
||||
android:padding="12dp"
|
||||
tools:text="THIS IS 2" />
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -24,7 +24,6 @@
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
tools:text="View all 10"
|
||||
style="@style/DetailsSecondaryButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
@ -8,11 +8,11 @@
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:theme="?attr/actionBarTheme"
|
||||
app:popupTheme="?attr/actionBarPopupTheme" />
|
||||
|
||||
@ -22,8 +22,8 @@
|
||||
android:layout_height="0dp"
|
||||
tools:listitem="@layout/installed_app_list_item"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<FrameLayout
|
||||
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">
|
||||
@ -11,9 +10,6 @@
|
||||
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" />
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
@ -1,33 +0,0 @@
|
||||
<?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"
|
||||
style="@style/DetailsSecondaryButtonStyle"
|
||||
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>
|
62
app/src/main/res/layout/updateable_app_list_item.xml
Normal file
62
app/src/main/res/layout/updateable_app_list_item.xml
Normal file
@ -0,0 +1,62 @@
|
||||
<?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:paddingStart="24dp"
|
||||
android:paddingLeft="24dp"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<!-- 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 (via the @+id/app_name). -->
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
tools:src="@drawable/ic_launcher"
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="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="16sp"
|
||||
android:textColor="#424242"
|
||||
android:lines="1"
|
||||
android:ellipsize="end"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
app:layout_constraintStart_toEndOf="@+id/icon"
|
||||
app:layout_constraintEnd_toStartOf="@+id/install"
|
||||
app:layout_constraintTop_toTopOf="@+id/icon"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/icon" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/install"
|
||||
tools:src="@drawable/ic_download"
|
||||
android:scaleType="fitXY"
|
||||
android:contentDescription="@string/updates__tts__download_app"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:elevation="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/icon"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/icon" />
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
87
app/src/main/res/layout/updateable_app_status_item.xml
Normal file
87
app/src/main/res/layout/updateable_app_status_item.xml
Normal file
@ -0,0 +1,87 @@
|
||||
<?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">
|
||||
|
||||
<!-- 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="16sp"
|
||||
android:textColor="#424242"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
app:layout_constraintStart_toEndOf="@+id/icon"
|
||||
app:layout_constraintEnd_toStartOf="@+id/action_button"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintVertical_bias="0.333" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/app_name"
|
||||
app:layout_constraintStart_toEndOf="@+id/icon"
|
||||
app:layout_constraintEnd_toStartOf="@+id/cancel_button"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/cancel_button"
|
||||
android:contentDescription="@string/cancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_cancel"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/progress_bar"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/progress_bar"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/action_button"
|
||||
style="@style/DetailsPrimaryButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Update" />
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
63
app/src/main/res/layout/updates_header.xml
Normal file
63
app/src/main/res/layout/updates_header.xml
Normal file
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.constraint.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_updates_available"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_download_all"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
tools:text="Download updates for 3 apps" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/button_download_all"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
app:srcCompat="@drawable/ic_download_progress_0"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:contentDescription="@string/updates__tts__download_updates_for_all_apps"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/text_updates_available" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_apps_to_update"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="SAnd, Birthday Droid, Dados D, Other app, Another app"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_download_all"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_updates_available" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_toggle_apps_to_update"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_apps_to_update"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:textColor="@color/fdroid_blue"
|
||||
android:padding="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
tools:text="Show apps"
|
||||
android:textAllCaps="true" />
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
@ -3,12 +3,12 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:title="@string/main_menu__latest_apps"
|
||||
android:icon="@drawable/ic_overview"
|
||||
android:icon="@drawable/ic_latest"
|
||||
app:showAsAction="ifRoom|withText"
|
||||
android:id="@+id/whats_new" />
|
||||
<item
|
||||
android:title="@string/main_menu__categories"
|
||||
android:icon="@drawable/ic_category"
|
||||
android:icon="@drawable/ic_categories"
|
||||
app:showAsAction="ifRoom|withText"
|
||||
android:id="@+id/categories" />
|
||||
<item
|
||||
@ -17,10 +17,10 @@
|
||||
app:showAsAction="ifRoom|withText"
|
||||
android:id="@+id/nearby" />
|
||||
<item
|
||||
android:title="@string/preference_category__my_apps"
|
||||
android:icon="@drawable/ic_my_apps"
|
||||
android:title="@string/updates"
|
||||
android:icon="@drawable/ic_updates"
|
||||
app:showAsAction="ifRoom|withText"
|
||||
android:id="@+id/my_apps" />
|
||||
android:id="@+id/updates" />
|
||||
<item
|
||||
android:title="@string/menu_settings"
|
||||
android:icon="@drawable/ic_settings"
|
||||
|
@ -29,4 +29,6 @@
|
||||
<dimen name="category_preview__padding__recycler_view__top">12dp</dimen>
|
||||
<dimen name="category_preview__padding__app_card__horizontal">3dp</dimen>
|
||||
<dimen name="category_preview__padding__app_card__vertical">4dp</dimen>
|
||||
|
||||
<dimen name="badge_size">18dp</dimen>
|
||||
</resources>
|
||||
|
@ -8,7 +8,4 @@
|
||||
<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>
|
@ -67,17 +67,31 @@
|
||||
<string name="app_version_x_installed">Version %1$s</string>
|
||||
<string name="app_recommended_version_installed">Version %1$s (Recommended)</string>
|
||||
<string name="app__newly_added">New</string>
|
||||
<string name="added_on">Added on %s</string>
|
||||
<string name="app__install_downloaded_update">Update</string>
|
||||
|
||||
<string name="app_list__name__downloaded_and_ready_to_update">Update %1$s</string>
|
||||
<string name="app_list__name__downloaded_and_ready_to_install">Install %1$s</string>
|
||||
<string name="app_list__name__downloading_in_progress">Downloading %1$s</string>
|
||||
<plurals name="app_list__age__released_x_days_ago">
|
||||
<item quantity="one">Released %1$d day ago</item>
|
||||
<item quantity="other">Released %1$d days ago</item>
|
||||
</plurals>
|
||||
|
||||
<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>
|
||||
<!-- The inline download button shown in the "Updates" screen only uses an icon and so requires
|
||||
some descriptive text for the TTS engine -->
|
||||
<string name="updates__tts__download_app">Download</string>
|
||||
<string name="updates__tts__download_updates_for_all_apps">Download all updates</string>
|
||||
|
||||
<string name="added_on">Added on %s</string>
|
||||
<string name="updates__hide_updateable_apps">Hide apps</string>
|
||||
<string name="updates__show_updateable_apps">Show apps</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 name="updates__download_updates_for_apps">
|
||||
<item quantity="one">Download update for %1$d app.</item>
|
||||
<item quantity="other">Download updates for %1$d apps.</item>
|
||||
</plurals>
|
||||
|
||||
<string name="ok">OK</string>
|
||||
|
@ -14,11 +14,17 @@
|
||||
<style name="DetailsPrimaryButtonStyle" parent="DetailsButtonStyle">
|
||||
<item name="android:textColor">#ffffff</item>
|
||||
<item name="android:background">@drawable/button_primary_background_selector</item>
|
||||
<item name="android:padding">8dp</item>
|
||||
<item name="android:minHeight">32dp</item>
|
||||
<item name="android:minWidth">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="DetailsSecondaryButtonStyle" parent="DetailsButtonStyle">
|
||||
<item name="android:textColor">@color/fdroid_blue</item>
|
||||
<item name="android:background">@drawable/button_secondary_background_selector</item>
|
||||
<item name="android:padding">8dp</item>
|
||||
<item name="android:minHeight">32dp</item>
|
||||
<item name="android:minWidth">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="DetailsMoreButtonStyle">
|
||||
|
Loading…
x
Reference in New Issue
Block a user