diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7888e3515..bb7e198ca 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -69,6 +69,7 @@ connected24: cat "$log" | curl --silent -F 'clbin=<-' https://clbin.com; done - exit $EXITVALUE + allow_failure: true after_script: # this file changes every time but should not be cached diff --git a/app/build.gradle b/app/build.gradle index 536765ef6..d7bedaf25 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,7 +50,7 @@ dependencies { testCompile 'junit:junit:4.12' - testCompile "org.robolectric:robolectric:3.1.2" + testCompile "org.robolectric:robolectric:3.3.1" // As per https://github.com/robolectric/robolectric/issues/1932#issuecomment-219796474 testCompile 'org.khronos:opengl-api:gl1.1-android-2.1_r1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3d280e78e..402e44597 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -135,8 +135,179 @@ android:launchMode="singleTop" android:windowSoftInputMode="adjustResize" android:configChanges="layoutDirection|locale|keyboardHidden|orientation|screenSize" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -215,6 +386,7 @@ + @@ -255,19 +427,6 @@ android:name="android.app.searchable" android:resource="@xml/searchable" /> - - - - - - - - - - @@ -280,7 +439,7 @@ The reason for this is that the only differentiating factor is the presence of a "swap=1" in the query string, and intent-filter is unable to deal with query parameters. An alternative would be to do something like fdroidswap:// as - a scheme, but then we. Need to copy/paste all of this intent-filter stuff and + a scheme, but then we need to copy/paste all of this intent-filter stuff and keep it up to date when it changes or a bug is found. --> @@ -337,179 +496,34 @@ - - - - - - - + + + + - + - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java index 8c8c980ca..ffb259ce8 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java @@ -16,6 +16,7 @@ import android.support.design.widget.CoordinatorLayout; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.support.v7.graphics.Palette; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; @@ -23,12 +24,13 @@ import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; -import android.widget.ImageView; +import android.view.View; import android.widget.Toast; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.assist.ImageScaleType; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; @@ -44,6 +46,7 @@ import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.DownloaderService; import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter; import org.fdroid.fdroid.views.ShareChooserDialog; +import org.fdroid.fdroid.views.apps.FeatureImage; public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog.ShareChooserDialogListener, AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks { @@ -95,15 +98,37 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog recyclerView.setAdapter(adapter); // Load the feature graphic, if present - if (!TextUtils.isEmpty(app.iconUrlLarge)) { - ImageView ivFeatureGraphic = (ImageView) findViewById(R.id.feature_graphic); - DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder() - .cacheInMemory(false) - .cacheOnDisk(true) - .imageScaleType(ImageScaleType.NONE) - .bitmapConfig(Bitmap.Config.RGB_565) - .build(); - ImageLoader.getInstance().displayImage(app.iconUrlLarge, ivFeatureGraphic, displayImageOptions); + if (!TextUtils.isEmpty(app.iconUrl)) { + final FeatureImage featureImage = (FeatureImage) findViewById(R.id.feature_graphic); + DisplayImageOptions displayImageOptions = Utils.getImageLoadingOptions().build(); + ImageLoader.getInstance().loadImage(app.iconUrl, displayImageOptions, new ImageLoadingListener() { + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + if (featureImage != null) { + new Palette.Builder(loadedImage).generate(new Palette.PaletteAsyncListener() { + @Override + public void onGenerated(Palette palette) { + featureImage.setPalette(palette); + } + }); + } + } + + @Override + public void onLoadingStarted(String imageUri, View view) { + + } + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + + } + }); } } diff --git a/app/src/main/java/org/fdroid/fdroid/NfcHelper.java b/app/src/main/java/org/fdroid/fdroid/NfcHelper.java index 7490c1598..81787c04b 100644 --- a/app/src/main/java/org/fdroid/fdroid/NfcHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NfcHelper.java @@ -38,7 +38,7 @@ public class NfcHelper { } @TargetApi(16) - static void setAndroidBeam(Activity activity, String packageName) { + public static void setAndroidBeam(Activity activity, String packageName) { if (Build.VERSION.SDK_INT < 16) { return; } diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index ea89f6e66..9a5e13215 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -31,6 +31,7 @@ import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import com.nostra13.universalimageloader.utils.DiskCacheUtils; import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.views.main.MainActivity; import java.util.ArrayList; @@ -408,7 +409,7 @@ class NotificationHelper { } // Intent to open main app list - Intent intentObject = new Intent(context, FDroid.class); + Intent intentObject = new Intent(context, MainActivity.class); PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0); NotificationCompat.Builder builder = @@ -483,7 +484,7 @@ class NotificationHelper { } // Intent to open main app list - Intent intentObject = new Intent(context, FDroid.class); + Intent intentObject = new Intent(context, MainActivity.class); PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0); NotificationCompat.Builder builder = diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java index cfe402d47..79ac07f55 100644 --- a/app/src/main/java/org/fdroid/fdroid/Preferences.java +++ b/app/src/main/java/org/fdroid/fdroid/Preferences.java @@ -68,6 +68,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh public static final String PREF_PROXY_PORT = "proxyPort"; public static final String PREF_SHOW_NFC_DURING_SWAP = "showNfcDuringSwap"; public static final String PREF_POST_PRIVILEGED_INSTALL = "postPrivilegedInstall"; + public static final String PREF_TRIED_EMPTY_UPDATE = "triedEmptyUpdate"; private static final boolean DEFAULT_ROOTED = true; private static final boolean DEFAULT_HIDE_ANTI_FEATURE_APPS = false; @@ -182,6 +183,20 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh } } + /** + * Used the first time F-Droid is installed to flag whether or not we have tried to request + * apps from the repo. This is used so that when there is no apps available, we can differentiate + * between whether the repos actually have no apps (in which case we don't need to continue + * asking), or whether there is no apps because we have never actually asked to update the repos. + */ + public boolean hasTriedEmptyUpdate() { + return preferences.getBoolean(PREF_TRIED_EMPTY_UPDATE, false); + } + + public void setTriedEmptyUpdate(boolean value) { + preferences.edit().putBoolean(PREF_TRIED_EMPTY_UPDATE, value).apply(); + } + public boolean getUnstableUpdates() { return preferences.getBoolean(PREF_UNSTABLE_UPDATES, DEFAULT_UNSTABLE_UPDATES); } diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 95336f431..4dcb1c000 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -50,6 +50,7 @@ import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.installer.InstallManagerService; +import org.fdroid.fdroid.views.main.MainActivity; import java.net.URL; import java.util.ArrayList; @@ -154,7 +155,7 @@ public class UpdateService extends IntentService { // http://stackoverflow.com/a/20032920 // if (Build.VERSION.SDK_INT <= 10) { - Intent pendingIntent = new Intent(this, FDroid.class); + Intent pendingIntent = new Intent(this, MainActivity.class); pendingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); notificationBuilder.setContentIntent(PendingIntent.getActivity(this, 0, pendingIntent, PendingIntent.FLAG_UPDATE_CURRENT)); } diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index e301bd74c..fdfe675d6 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -569,6 +569,10 @@ public class App extends ValueObject implements Comparable, Parcelable { return TextUtils.isEmpty(flattrID) ? null : "https://flattr.com/thing/" + flattrID; } + /** + * @see App#suggestedVersionName for why this uses a getter while other member variables are + * publicly accessible. + */ public String getSuggestedVersionName() { return suggestedVersionName; } diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java index 349f71fa0..c4cf76521 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -199,26 +199,16 @@ public class AppProvider extends FDroidProvider { public AppQuerySelection add(AppQuerySelection query) { QuerySelection both = super.add(query); AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs()); - ensureJoinsCopied(query, bothWithJoin); + if (this.naturalJoinToInstalled() || query.naturalJoinToInstalled()) { + bothWithJoin.requireNaturalInstalledTable(); + } + + if (this.leftJoinToPrefs() || query.leftJoinToPrefs()) { + bothWithJoin.requireLeftJoinPrefs(); + } return bothWithJoin; } - public AppQuerySelection not(AppQuerySelection query) { - QuerySelection both = super.not(query); - AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs()); - ensureJoinsCopied(query, bothWithJoin); - return bothWithJoin; - } - - private void ensureJoinsCopied(AppQuerySelection toAdd, AppQuerySelection newlyCreated) { - if (this.naturalJoinToInstalled() || toAdd.naturalJoinToInstalled()) { - newlyCreated.requireNaturalInstalledTable(); - } - - if (this.leftJoinToPrefs() || toAdd.leftJoinToPrefs()) { - newlyCreated.requireLeftJoinPrefs(); - } - } } protected class Query extends QueryBuilder { @@ -574,8 +564,7 @@ public class AppProvider extends FDroidProvider { final String ignoreAll = "COALESCE(prefs." + AppPrefsTable.Cols.IGNORE_ALL_UPDATES + ", 0) != 1"; final String ignore = " (" + ignoreCurrent + " AND " + ignoreAll + ") "; - final String nullChecks = app + "." + Cols.SUGGESTED_VERSION_CODE + " IS NOT NULL AND installed." + InstalledAppTable.Cols.VERSION_CODE + " IS NOT NULL "; - final String where = nullChecks + " AND " + ignore + " AND " + app + "." + Cols.SUGGESTED_VERSION_CODE + " > installed." + InstalledAppTable.Cols.VERSION_CODE; + final String where = ignore + " AND " + app + "." + Cols.SUGGESTED_VERSION_CODE + " > installed." + InstalledAppTable.Cols.VERSION_CODE; return new AppQuerySelection(where).requireNaturalInstalledTable().requireLeftJoinPrefs(); } @@ -587,7 +576,7 @@ public class AppProvider extends FDroidProvider { } private AppQuerySelection queryInstalled() { - return new AppQuerySelection().requireNaturalInstalledTable().not(queryCanUpdate()); + return new AppQuerySelection().requireNaturalInstalledTable(); } private AppQuerySelection querySearch(String query) { @@ -801,7 +790,11 @@ public class AppProvider extends FDroidProvider { break; case RECENTLY_UPDATED: - sortOrder = getTableName() + "." + Cols.LAST_UPDATED + " DESC"; + String table = getTableName(); + String isNew = table + "." + Cols.LAST_UPDATED + " <= " + table + "." + Cols.ADDED + " DESC"; + String lastUpdated = table + "." + Cols.LAST_UPDATED + " DESC"; + sortOrder = lastUpdated + ", " + isNew; + selection = selection.add(queryRecentlyUpdated()); includeSwap = false; break; diff --git a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java index 76a4f9312..223a2197b 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -28,6 +28,7 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; @@ -880,7 +881,8 @@ class DBHelper extends SQLiteOpenHelper { private void resetTransient(SQLiteDatabase db) { Utils.debugLog(TAG, "Removing app + apk tables so they can be recreated. Next time F-Droid updates it should trigger an index update."); - context.getSharedPreferences("FDroid", Context.MODE_PRIVATE) + + PreferenceManager.getDefaultSharedPreferences(context) .edit() .putBoolean("triedEmptyUpdate", false) .apply(); @@ -924,8 +926,12 @@ class DBHelper extends SQLiteOpenHelper { if (oldVersion >= 42) { return; } - context.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit() - .putBoolean("triedEmptyUpdate", false).apply(); + + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean("triedEmptyUpdate", false) + .apply(); + db.execSQL("drop table " + AppMetadataTable.NAME); db.execSQL("drop table " + ApkTable.NAME); clearRepoEtags(db); diff --git a/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java b/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java index d5461799f..5c32395a7 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java +++ b/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java @@ -78,8 +78,4 @@ public class QuerySelection { return new QuerySelection(s, a); } - public QuerySelection not(QuerySelection querySelection) { - String where = " NOT (" + querySelection.getSelection() + ") "; - return add(where, querySelection.getArgs()); - } } diff --git a/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java b/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java index d56d39cc9..43a34177f 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/WifiStateChangeService.java @@ -62,7 +62,7 @@ public class WifiStateChangeService extends IntentService { } Utils.debugLog(TAG, "WiFi change service started, clearing info about wifi state until we have figured it out again."); NetworkInfo ni = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); - wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); + wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE); int wifiState = wifiManager.getWifiState(); if (ni == null || ni.isConnected()) { Utils.debugLog(TAG, "ni == " + ni + " wifiState == " + printWifiState(wifiState)); diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java b/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java index c6be97c07..6efd9b25a 100644 --- a/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java @@ -32,10 +32,10 @@ import android.text.Html; import android.util.Log; import android.view.ContextThemeWrapper; -import org.fdroid.fdroid.FDroid; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; import org.fdroid.fdroid.installer.PrivilegedInstaller; +import org.fdroid.fdroid.views.main.MainActivity; import java.io.File; @@ -259,7 +259,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity { public void onClick(DialogInterface dialogInterface, int i) { InstallExtensionDialogActivity.this.setResult(result); InstallExtensionDialogActivity.this.finish(); - startActivity(new Intent(InstallExtensionDialogActivity.this, FDroid.class)); + startActivity(new Intent(InstallExtensionDialogActivity.this, MainActivity.class)); } }) .setCancelable(false); diff --git a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java index 13407e616..d9db24604 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java @@ -32,7 +32,6 @@ import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.LoaderManager; -import android.support.v4.app.NavUtils; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v7.app.AlertDialog; @@ -52,7 +51,6 @@ import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; -import org.fdroid.fdroid.FDroid; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; @@ -149,11 +147,6 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case android.R.id.home: - Intent destIntent = new Intent(this, FDroid.class); - setResult(RESULT_OK, destIntent); - NavUtils.navigateUpTo(this, destIntent); - return true; case R.id.action_add_repo: showAddRepo(); return true; @@ -679,7 +672,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana private void checkIfNewRepoOnSameWifi(NewRepoConfig newRepo) { // if this is a local repo, check we're on the same wifi if (!TextUtils.isEmpty(newRepo.getBssid())) { - WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); + WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); WifiInfo wifiInfo = wifiManager.getConnectionInfo(); String bssid = wifiInfo.getBSSID(); if (TextUtils.isEmpty(bssid)) { /* not all devices have wifi */ diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java index 4b68022f7..c8c3e777b 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java @@ -11,8 +11,12 @@ 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.KeyEvent; import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import android.widget.TextView; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.AppProvider; @@ -21,6 +25,7 @@ import org.fdroid.fdroid.data.Schema; public class AppListActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks, CategoryTextWatcher.SearchTermsChangedListener { public static final String EXTRA_CATEGORY = "org.fdroid.fdroid.views.apps.AppListActivity.EXTRA_CATEGORY"; + public static final String EXTRA_SEARCH_TERMS = "org.fdroid.fdroid.views.apps.AppListActivity.EXTRA_SEARCH_TERMS"; private RecyclerView appView; private AppListAdapter appAdapter; private String category; @@ -35,6 +40,21 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. searchInput = (EditText) findViewById(R.id.search); searchInput.addTextChangedListener(new CategoryTextWatcher(this, searchInput, this)); + searchInput.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + // Hide the keyboard (http://stackoverflow.com/a/1109108 (when pressing search) + InputMethodManager inputManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + inputManager.hideSoftInputFromWindow(searchInput.getWindowToken(), 0); + + // Change focus from the search input to the app list. + appView.requestFocus(); + return true; + } + return false; + } + }); View backButton = findViewById(R.id.back); backButton.setOnClickListener(new View.OnClickListener() { @@ -66,8 +86,9 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. Intent intent = getIntent(); category = intent.hasExtra(EXTRA_CATEGORY) ? intent.getStringExtra(EXTRA_CATEGORY) : null; + searchTerms = intent.hasExtra(EXTRA_SEARCH_TERMS) ? intent.getStringExtra(EXTRA_SEARCH_TERMS) : null; - searchInput.setText(getSearchText(category, null)); + searchInput.setText(getSearchText(category, searchTerms)); searchInput.setSelection(searchInput.getText().length()); if (category != null) { diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java index 3f2692349..8d590540e 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java @@ -1,15 +1,25 @@ package org.fdroid.fdroid.views.apps; +import android.annotation.TargetApi; import android.app.Activity; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.graphics.Outline; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.app.ActivityOptionsCompat; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.LocalBroadcastManager; import android.support.v4.util.Pair; import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; import android.view.View; -import android.widget.Button; +import android.view.ViewOutlineProvider; import android.widget.ImageView; import android.widget.TextView; @@ -18,34 +28,85 @@ import com.nostra13.universalimageloader.core.ImageLoader; import org.fdroid.fdroid.AppDetails; import org.fdroid.fdroid.AppDetails2; +import org.fdroid.fdroid.AppUpdateStatusManager; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppPrefs; +import org.fdroid.fdroid.installer.ApkCache; import org.fdroid.fdroid.installer.InstallManagerService; +import org.fdroid.fdroid.installer.Installer; +import org.fdroid.fdroid.installer.InstallerFactory; +import org.fdroid.fdroid.net.Downloader; +import org.fdroid.fdroid.net.DownloaderService; +import java.io.File; + +// TODO: Support cancelling of downloads by tapping the install button a second time. +// TODO: Support installing of an app once downloaded by tapping the install button a second time. public class AppListItemController extends RecyclerView.ViewHolder { + private static final String TAG = "AppListItemController"; + private final Activity activity; - private final Button installButton; + @NonNull private final ImageView icon; + + @NonNull private final TextView name; + + @Nullable + private final ImageView installButton; + + @Nullable private final TextView status; + + @Nullable + private final TextView installedVersion; + + @Nullable + private final TextView ignoredStatus; + private final DisplayImageOptions displayImageOptions; private App currentApp; + private String currentAppDownloadUrl; - public AppListItemController(Activity activity, View itemView) { + @TargetApi(21) + public AppListItemController(final Activity activity, View itemView) { super(itemView); this.activity = activity; - installButton = (Button) itemView.findViewById(R.id.install); - installButton.setOnClickListener(onInstallClicked); + installButton = (ImageView) itemView.findViewById(R.id.install); + if (installButton != null) { + installButton.setOnClickListener(onInstallClicked); + + if (Build.VERSION.SDK_INT >= 21) { + installButton.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + float density = activity.getResources().getDisplayMetrics().density; + + // TODO: This is a bit hacky/hardcoded/too-specific to the particular icons we're using. + // This is because the default "download & install" and "downloaded & ready to install" + // icons are smaller than the "downloading progress" button. Hence, we can't just use + // the width/height of the view to calculate the outline size. + int xPadding = (int) (8 * density); + int yPadding = (int) (9 * density); + outline.setOval(xPadding, yPadding, installButton.getWidth() - xPadding, installButton.getHeight() - yPadding); + } + }); + } + } icon = (ImageView) itemView.findViewById(R.id.icon); name = (TextView) itemView.findViewById(R.id.app_name); status = (TextView) itemView.findViewById(R.id.status); + installedVersion = (TextView) itemView.findViewById(R.id.installed_version); + ignoredStatus = (TextView) itemView.findViewById(R.id.ignored_status); displayImageOptions = Utils.getImageLoadingOptions().build(); @@ -58,7 +119,19 @@ public class AppListItemController extends RecyclerView.ViewHolder { ImageLoader.getInstance().displayImage(app.iconUrl, icon, displayImageOptions); + Apk apkToInstall = ApkProvider.Helper.findApkFromAnyRepo(activity, app.packageName, app.suggestedVersionCode); + currentAppDownloadUrl = apkToInstall.getUrl(); + + final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity.getApplicationContext()); + broadcastManager.unregisterReceiver(onDownloadProgress); + broadcastManager.unregisterReceiver(onInstallAction); + + broadcastManager.registerReceiver(onDownloadProgress, DownloaderService.getIntentFilter(currentAppDownloadUrl)); + broadcastManager.registerReceiver(onInstallAction, Installer.getInstallIntentFilter(Uri.parse(currentAppDownloadUrl))); + configureStatusText(app); + configureInstalledVersion(app); + configureIgnoredStatus(app); configureInstallButton(app); } @@ -94,6 +167,51 @@ public class AppListItemController extends RecyclerView.ViewHolder { } + /** + * Shows the currently installed version name, and whether or not it is the recommended version. + * Binds to the {@link R.id#installed_version} {@link TextView}. + */ + private void configureInstalledVersion(@NonNull App app) { + if (installedVersion == null) { + return; + } + + int res = (app.suggestedVersionCode == app.installedVersionCode) + ? R.string.app_recommended_version_installed : R.string.app_version_x_installed; + + installedVersion.setText(activity.getString(res, app.installedVersionName)); + } + + /** + * Shows whether the user has previously asked to ignore updates for this app entirely, or for a + * specific version of this app. Binds to the {@link R.id#ignored_status} {@link TextView}. + */ + private void configureIgnoredStatus(@NonNull App app) { + if (ignoredStatus == null) { + return; + } + + AppPrefs prefs = app.getPrefs(activity); + if (prefs.ignoreAllUpdates) { + ignoredStatus.setText(activity.getString(R.string.installed_app__updates_ignored)); + ignoredStatus.setVisibility(View.VISIBLE); + } else if (prefs.ignoreThisUpdate > 0 && prefs.ignoreThisUpdate == app.suggestedVersionCode) { + ignoredStatus.setText(activity.getString(R.string.installed_app__updates_ignored_for_suggested_version, app.getSuggestedVersionName())); + ignoredStatus.setVisibility(View.VISIBLE); + } else { + ignoredStatus.setVisibility(View.GONE); + } + } + + private boolean isReadyToInstall(@NonNull App app) { + for (AppUpdateStatusManager.AppUpdateStatus appStatus : AppUpdateStatusManager.getInstance(activity).getByPackageName(app.packageName)) { + if (appStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) { + return true; + } + } + return false; + } + /** * The install button is shown when an app: * * Is compatible with the users device. @@ -107,16 +225,24 @@ public class AppListItemController extends RecyclerView.ViewHolder { return; } - boolean installable = app.canAndWantToUpdate(activity) || !app.isInstalled(); - boolean shouldAllow = app.compatible && !app.isFiltered(); - - if (shouldAllow && installable) { + if (isReadyToInstall(app)) { + installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_complete)); installButton.setVisibility(View.VISIBLE); + // TODO: If in the downloading phase, then need to reflect that instead of this "download complete" icon. } else { - installButton.setVisibility(View.GONE); + boolean installable = app.canAndWantToUpdate(activity) || !app.isInstalled(); + boolean shouldAllow = app.compatible && !app.isFiltered(); + + if (shouldAllow && installable) { + installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download)); + installButton.setVisibility(View.VISIBLE); + } else { + installButton.setVisibility(View.GONE); + } } } + @SuppressWarnings("FieldCanBeLocal") private final View.OnClickListener onAppClicked = new View.OnClickListener() { @Override public void onClick(View v) { @@ -136,6 +262,52 @@ public class AppListItemController extends RecyclerView.ViewHolder { } }; + private final BroadcastReceiver onDownloadProgress = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (installButton == null || currentApp == null || !TextUtils.equals(currentAppDownloadUrl, intent.getDataString())) { + 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); + + int progressAsDegrees = (int) (((float) bytesRead / totalBytes) * 360); + installButton.setImageLevel(progressAsDegrees); + } else if (Downloader.ACTION_COMPLETE.equals(intent.getAction())) { + installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_complete)); + } + } + }; + + private final BroadcastReceiver onInstallAction = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (currentApp == null || installButton == null) { + return; + } + + Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK); + if (!TextUtils.equals(apk.packageName, currentApp.packageName)) { + return; + } + + if (Installer.ACTION_INSTALL_STARTED.equals(intent.getAction())) { + installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_progress)); + installButton.setImageLevel(0); + } else if (Installer.ACTION_INSTALL_COMPLETE.equals(intent.getAction())) { + installButton.setVisibility(View.GONE); + // TODO: It could've been a different version other than the current suggested version. + // In these cases, don't hide the button but rather set it back to the default install image. + } else if (Installer.ACTION_INSTALL_INTERRUPTED.equals(intent.getAction())) { + installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download)); + } + } + }; + + @SuppressWarnings("FieldCanBeLocal") private final View.OnClickListener onInstallClicked = new View.OnClickListener() { @Override public void onClick(View v) { @@ -143,7 +315,36 @@ public class AppListItemController extends RecyclerView.ViewHolder { return; } - InstallManagerService.queue(activity, currentApp, ApkProvider.Helper.findApkFromAnyRepo(activity, currentApp.packageName, currentApp.suggestedVersionCode)); + final Apk suggestedApk = ApkProvider.Helper.findApkFromAnyRepo(activity, currentApp.packageName, currentApp.suggestedVersionCode); + + if (isReadyToInstall(currentApp)) { + File apkFilePath = ApkCache.getApkDownloadPath(activity, Uri.parse(suggestedApk.getUrl())); + Utils.debugLog(TAG, "skip download, we have already downloaded " + suggestedApk.getUrl() + " to " + apkFilePath); + + // TODO: This seems like a bit of a hack. Is there a better way to do this by changing + // the Installer API so that we can ask it to install without having to get it to fire + // off an intent which we then listen for and action? + final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity); + final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + broadcastManager.unregisterReceiver(this); + + if (Installer.ACTION_INSTALL_USER_INTERACTION.equals(intent.getAction())) { + PendingIntent pendingIntent = intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI); + try { + pendingIntent.send(); + } catch (PendingIntent.CanceledException ignored) { } + } + } + }; + + broadcastManager.registerReceiver(receiver, Installer.getInstallIntentFilter(Uri.parse(suggestedApk.getUrl()))); + Installer installer = InstallerFactory.create(activity, suggestedApk); + installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), Uri.parse(suggestedApk.getUrl())); + } else { + InstallManagerService.queue(activity, currentApp, suggestedApk); + } } }; } diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/CategoryTextWatcher.java b/app/src/main/java/org/fdroid/fdroid/views/apps/CategoryTextWatcher.java index daad4a208..2d943cfae 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/CategoryTextWatcher.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/CategoryTextWatcher.java @@ -139,7 +139,8 @@ public class CategoryTextWatcher implements TextWatcher { if (Build.VERSION.SDK_INT >= 21) { // For accessibility reasons, make this more clear to screen readers that the // span we just added semantically represents a category. - TtsSpan ttsSpan = new TtsSpan.TextBuilder(context.getString(R.string.category)).build(); + CharSequence categoryName = textToSpannify.subSequence(0, colonIndex); + TtsSpan ttsSpan = new TtsSpan.TextBuilder(context.getString(R.string.tts_category_name, categoryName)).build(); textToSpannify.setSpan(ttsSpan, 0, 0, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/FeatureImage.java b/app/src/main/java/org/fdroid/fdroid/views/apps/FeatureImage.java new file mode 100644 index 000000000..b43dc2bb9 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/FeatureImage.java @@ -0,0 +1,228 @@ +package org.fdroid.fdroid.views.apps; + +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.v7.graphics.Palette; +import android.support.v7.widget.AppCompatImageView; +import android.util.AttributeSet; + +import java.util.Random; + +/** + * A feature image can have a {@link android.graphics.drawable.Drawable} or a {@link Palette}. If + * a Drawable is available, then it will draw that, otherwise it will attempt to fall back to the + * Palette you gave it. If a Palette is given, it will draw a series of triangles like so: + * + * +_----+----_+_----+----_+ + * | \_ | _/ | \_ | _/ | + * | \_|_/ | \_|_/ | + * +_----+----_+_----+----_+ + * | \_ | _/ | \_ | _/ | + * | \_|_/ | \_|_/ | + * +-----+-----+-----+-----+ + * + * where each triangle is filled with one of two variations of the {@link Palette#getDominantColor(int)} + * that is chosen randomly. The randomness is first seeded with the colour that has been selected. + * This is so that if this repaints itself in the future, it will have the same unique pattern rather + * than picking a new random pattern each time. + * + * It is suggested that you obtain the Palette from the icon of an app. + */ +public class FeatureImage extends AppCompatImageView { + + private static final int NUM_SQUARES_WIDE = 4; + private static final int NUM_SQUARES_HIGH = 2; + + // Double, because there are two triangles per square. + private final Path[] triangles = new Path[NUM_SQUARES_HIGH * NUM_SQUARES_WIDE * 2]; + + @Nullable + private Paint[] trianglePaints; + + private static final Paint WHITE_PAINT = new Paint(); + + static { + WHITE_PAINT.setColor(Color.WHITE); + WHITE_PAINT.setStyle(Paint.Style.FILL); + } + + public FeatureImage(Context context) { + super(context); + } + + public FeatureImage(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public FeatureImage(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + /** + * Takes the {@link Palette#getDominantColor(int)} from the palette, dims it substantially, and + * then creates a second variation that is slightly dimmer still. These two colours are then + * randomly allocated to each triangle which is expected to be rendered. + */ + public void setPalette(@Nullable Palette palette) { + if (palette == null) { + trianglePaints = null; + return; + } + + // It is easier to dull al colour in the HSV space, so convert to that then adjust the + // saturation down and the colour value down. + float[] hsv = new float[3]; + Color.colorToHSV(palette.getDominantColor(Color.LTGRAY), hsv); + hsv[1] *= 0.5f; + hsv[2] *= 0.7f; + int colourOne = Color.HSVToColor(hsv); + + hsv[2] *= 0.9f; + int colourTwo = Color.HSVToColor(hsv); + + Paint paintOne = new Paint(); + paintOne.setColor(colourOne); + paintOne.setAntiAlias(true); + paintOne.setStrokeWidth(2); + paintOne.setStyle(Paint.Style.FILL_AND_STROKE); + + Paint paintTwo = new Paint(); + paintTwo.setColor(colourTwo); + paintTwo.setAntiAlias(true); + paintTwo.setStrokeWidth(2); + paintTwo.setStyle(Paint.Style.FILL_AND_STROKE); + + // Seed based on the colour, so that each time we try to render a feature image with the + // same colour, it will give the same pattern. + Random random = new Random(colourOne); + trianglePaints = new Paint[triangles.length]; + for (int i = 0; i < trianglePaints.length; i++) { + trianglePaints[i] = random.nextBoolean() ? paintOne : paintTwo; + } + + animateColourChange(); + } + + private int currentAlpha = 255; + private ValueAnimator alphaAnimator = null; + + @TargetApi(11) + private void animateColourChange() { + if (Build.VERSION.SDK_INT < 11) { + return; + } + + if (alphaAnimator == null) { + alphaAnimator = ValueAnimator.ofInt(0, 255); + } else { + alphaAnimator.cancel(); + } + + alphaAnimator = ValueAnimator.ofInt(0, 255).setDuration(150); + alphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + currentAlpha = (int) animation.getAnimatedValue(); + invalidate(); + } + }); + + currentAlpha = 0; + invalidate(); + alphaAnimator.start(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + int triangleWidth = w / NUM_SQUARES_WIDE; + int triangleHeight = h / NUM_SQUARES_HIGH; + + for (int x = 0; x < NUM_SQUARES_WIDE; x++) { + for (int y = 0; y < NUM_SQUARES_HIGH; y++) { + int startX = x * triangleWidth; + int startY = y * triangleHeight; + int endX = startX + triangleWidth; + int endY = startY + triangleHeight; + + // Note that the order of these points need to go in a clockwise direction, or else + // the fill will not be applied properly. + Path firstTriangle; + Path secondTriangle; + + // Alternate between two different ways to split a square into two triangles. This + // results in a nicer geometric pattern (see doc comments at top of class for more + // ASCII art of the expected outcome). + if (x % 2 == 0) { + // +_----+ + // | \_ 1| + // |2 \_| + // +-----+ + firstTriangle = createTriangle(new Point(startX, startY), new Point(endX, startY), new Point(endX, endY)); + secondTriangle = createTriangle(new Point(startX, startY), new Point(endX, endY), new Point(startX, endY)); + } else { + // +----_+ + // |1 _/ | + // |_/ 2| + // +-----+ + firstTriangle = createTriangle(new Point(startX, startY), new Point(endX, startY), new Point(startX, endY)); + secondTriangle = createTriangle(new Point(startX, endY), new Point(endX, startY), new Point(endX, endY)); + } + + triangles[y * (NUM_SQUARES_WIDE * 2) + (x * 2)] = firstTriangle; + triangles[y * (NUM_SQUARES_WIDE * 2) + (x * 2) + 1] = secondTriangle; + } + } + + } + + /** + * First try to draw whatever image was given to this view. If that doesn't exist, try to draw + * a geometric pattern based on the palette that was given to us. If we haven't had a palette + * assigned to us (using {@link FeatureImage#setPalette(Palette)}) then clear the + * view by filling it with white. + */ + @Override + protected void onDraw(Canvas canvas) { + if (getDrawable() != null) { + super.onDraw(canvas); + } else if (trianglePaints != null) { + for (Paint paint : trianglePaints) { + paint.setAlpha(currentAlpha); + } + + canvas.drawRect(0, 0, getWidth(), getHeight(), WHITE_PAINT); + for (int i = 0; i < triangles.length; i++) { + canvas.drawPath(triangles[i], trianglePaints[i]); + } + } else { + canvas.drawRect(0, 0, getWidth(), getHeight(), WHITE_PAINT); + } + + } + + /** + * This requires the three points to be in a sequence that traces out a triangle in clockwise + * fashion. This is required for the triangle to be filled correctly when drawing, otherwise + * it will end up black. + */ + private static Path createTriangle(Point start, Point middle, Point end) { + Path path = new Path(); + path.setFillType(Path.FillType.EVEN_ODD); + path.moveTo(start.x, start.y); + path.lineTo(middle.x, middle.y); + path.lineTo(end.x, end.y); + path.close(); + + return path; + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java index b625ec1bd..cefe85d6f 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java @@ -23,57 +23,59 @@ import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import org.fdroid.fdroid.AppDetails; import org.fdroid.fdroid.AppDetails2; -import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.App; - -import java.util.Date; +import org.fdroid.fdroid.views.apps.FeatureImage; /** * The {@link AppCardController} can bind an app to several different layouts, as long as the layout * contains the following elements: * + {@link R.id#icon} ({@link ImageView}, required) * + {@link R.id#summary} ({@link TextView}, required) + * + {@link R.id#new_tag} ({@link TextView}, optional) * + {@link R.id#featured_image} ({@link ImageView}, optional) - * + {@link R.id#status} ({@link TextView}, optional) */ public class AppCardController extends RecyclerView.ViewHolder implements ImageLoadingListener, View.OnClickListener { @NonNull private final ImageView icon; + /** + * Text starting with the app name (in bold) followed by a short summary of the app. + */ @NonNull private final TextView summary; + /** + * A little blue tag which says "New" to indicate an app was added to the repository recently. + */ @Nullable - private final TextView status; + private final TextView newTag; + /** + * Wide and short image for branding the app. If it is not present in the metadata then F-Droid + * will draw some abstract art instead. + */ @Nullable - private final ImageView featuredImage; + private final FeatureImage featuredImage; @Nullable private App currentApp; private final Activity activity; - private final int defaultFeaturedImageColour; private final DisplayImageOptions displayImageOptions; - private final Date recentCuttoffDate; - public AppCardController(Activity activity, View itemView) { super(itemView); this.activity = activity; - recentCuttoffDate = Preferences.get().calcMaxHistory(); - icon = (ImageView) findViewAndEnsureNonNull(itemView, R.id.icon); summary = (TextView) findViewAndEnsureNonNull(itemView, R.id.summary); - featuredImage = (ImageView) itemView.findViewById(R.id.featured_image); - status = (TextView) itemView.findViewById(R.id.status); + featuredImage = (FeatureImage) itemView.findViewById(R.id.featured_image); + newTag = (TextView) itemView.findViewById(R.id.new_tag); - defaultFeaturedImageColour = activity.getResources().getColor(R.color.cardview_light_background); displayImageOptions = Utils.getImageLoadingOptions().build(); itemView.setOnClickListener(this); @@ -100,20 +102,17 @@ public class AppCardController extends RecyclerView.ViewHolder implements ImageL summary.setText(Utils.formatAppNameAndSummary(app.name, app.summary)); - if (status != null) { - if (app.added != null && app.added.after(recentCuttoffDate) && (app.lastUpdated == null || app.added.equals(app.lastUpdated))) { - status.setText(activity.getString(R.string.category_Whats_New)); - status.setVisibility(View.VISIBLE); - } else if (app.lastUpdated != null && app.lastUpdated.after(recentCuttoffDate)) { - status.setText(activity.getString(R.string.category_Recently_Updated)); - status.setVisibility(View.VISIBLE); + if (newTag != null) { + if (app.added != null && app.lastUpdated != null && app.added.equals(app.lastUpdated)) { + newTag.setVisibility(View.VISIBLE); } else { - status.setVisibility(View.GONE); + newTag.setVisibility(View.GONE); } } if (featuredImage != null) { - featuredImage.setBackgroundColor(defaultFeaturedImageColour); + featuredImage.setPalette(null); + featuredImage.setImageDrawable(null); } ImageLoader.getInstance().displayImage(app.iconUrl, icon, displayImageOptions, this); @@ -151,12 +150,11 @@ public class AppCardController extends RecyclerView.ViewHolder implements ImageL @Override public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { - final ImageView image = featuredImage; - if (image != null) { + if (featuredImage != null) { new Palette.Builder(loadedImage).generate(new Palette.PaletteAsyncListener() { @Override public void onGenerated(Palette palette) { - image.setBackgroundColor(palette.getDominantColor(defaultFeaturedImageColour)); + featuredImage.setPalette(palette); } }); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java index 402239f0c..822236a23 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java @@ -121,6 +121,7 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade int numAppsInCategory = cursor.getInt(0); viewAll.setVisibility(View.VISIBLE); viewAll.setText(activity.getResources().getQuantityString(R.plurals.button_view_all_apps_in_category, numAppsInCategory, numAppsInCategory)); + viewAll.setContentDescription(activity.getResources().getQuantityString(R.plurals.tts_view_all_in_category, numAppsInCategory, numAppsInCategory, currentCategory)); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/fragments/AppListFragment.java b/app/src/main/java/org/fdroid/fdroid/views/fragments/AppListFragment.java index 702b2470d..3a91af37c 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/fragments/AppListFragment.java +++ b/app/src/main/java/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -1,8 +1,6 @@ package org.fdroid.fdroid.views.fragments; -import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.os.Build; @@ -143,12 +141,10 @@ public abstract class AppListFragment extends ListFragment implements * be bad. */ private boolean updateEmptyRepos() { - final String triedEmptyUpdate = "triedEmptyUpdate"; - SharedPreferences prefs = getActivity().getPreferences(Context.MODE_PRIVATE); - boolean hasTriedEmptyUpdate = prefs.getBoolean(triedEmptyUpdate, false); - if (!hasTriedEmptyUpdate) { + Preferences prefs = Preferences.get(); + if (!prefs.hasTriedEmptyUpdate()) { Utils.debugLog(TAG, "Empty app list, and we haven't done an update yet. Forcing repo update."); - prefs.edit().putBoolean(triedEmptyUpdate, true).apply(); + prefs.setTriedEmptyUpdate(true); UpdateService.updateNow(getActivity()); return true; } diff --git a/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppsActivity.java new file mode 100644 index 000000000..6f051dd6d --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppsActivity.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com + * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.fdroid.fdroid.views.installed; + +import android.app.Activity; +import android.database.Cursor; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.view.ViewGroup; + +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.Schema; +import org.fdroid.fdroid.views.apps.AppListItemController; + +public class InstalledAppsActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks { + + private InstalledAppListAdapter adapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + + ((FDroidApp) getApplication()).applyTheme(this); + super.onCreate(savedInstanceState); + + setContentView(R.layout.installed_apps_layout); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + toolbar.setTitle(getString(R.string.installed_apps__activity_title)); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + adapter = new InstalledAppListAdapter(this); + + RecyclerView appList = (RecyclerView) findViewById(R.id.app_list); + appList.setHasFixedSize(true); + appList.setLayoutManager(new LinearLayoutManager(this)); + appList.setAdapter(adapter); + } + + @Override + protected void onResume() { + super.onResume(); + + // Starts a new or restarts an existing Loader in this manager + getSupportLoaderManager().restartLoader(0, null, this); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new CursorLoader( + this, + AppProvider.getInstalledUri(), + Schema.AppMetadataTable.Cols.ALL, + null, null, null); + } + + @Override + public void onLoadFinished(Loader loader, Cursor cursor) { + adapter.setApps(cursor); + } + + @Override + public void onLoaderReset(Loader loader) { + adapter.setApps(null); + } + + static class InstalledAppListAdapter extends RecyclerView.Adapter { + + private final Activity activity; + + @Nullable + private Cursor cursor; + + InstalledAppListAdapter(Activity activity) { + this.activity = activity; + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + if (cursor == null) { + return 0; + } + + cursor.moveToPosition(position); + return cursor.getLong(cursor.getColumnIndex(Schema.AppMetadataTable.Cols.ROW_ID)); + } + + @Override + public AppListItemController onCreateViewHolder(ViewGroup parent, int viewType) { + View view = activity.getLayoutInflater().inflate(R.layout.installed_app_list_item, parent, false); + return new AppListItemController(activity, view); + } + + @Override + public void onBindViewHolder(AppListItemController holder, int position) { + if (cursor == null) { + return; + } + + cursor.moveToPosition(position); + holder.bindModel(new App(cursor)); + } + + @Override + public int getItemCount() { + return cursor == null ? 0 : cursor.getCount(); + } + + public void setApps(@Nullable Cursor cursor) { + this.cursor = cursor; + notifyDataSetChanged(); + } + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java b/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java index 3bcf6567c..9528f7ffc 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java @@ -1,7 +1,9 @@ package org.fdroid.fdroid.views.main; +import android.content.Intent; import android.database.Cursor; import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; @@ -14,6 +16,7 @@ import android.widget.FrameLayout; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.CategoryProvider; import org.fdroid.fdroid.data.Schema; +import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.categories.CategoryAdapter; /** @@ -28,7 +31,7 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { private final CategoryAdapter categoryAdapter; private final AppCompatActivity activity; - CategoriesViewBinder(AppCompatActivity activity, FrameLayout parent) { + CategoriesViewBinder(final AppCompatActivity activity, FrameLayout parent) { this.activity = activity; View categoriesView = activity.getLayoutInflater().inflate(R.layout.main_tab_categories, parent, true); @@ -40,6 +43,14 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { categoriesList.setLayoutManager(new LinearLayoutManager(activity)); categoriesList.setAdapter(categoryAdapter); + FloatingActionButton searchFab = (FloatingActionButton) categoriesView.findViewById(R.id.btn_search); + searchFab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + activity.startActivity(new Intent(activity, AppListActivity.class)); + } + }); + activity.getSupportLoaderManager().initLoader(LOADER_ID, null, this); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java b/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java index 649231f16..8d62b8f9b 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java @@ -1,15 +1,32 @@ package org.fdroid.fdroid.views.main; +import android.app.SearchManager; import android.content.Context; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.design.widget.BottomNavigationView; 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 org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.AppDetails2; +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.NfcHelper; +import org.fdroid.fdroid.Preferences; 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.NewRepoConfig; +import org.fdroid.fdroid.views.ManageReposActivity; +import org.fdroid.fdroid.views.apps.AppListActivity; +import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; /** * Main view shown to users upon starting F-Droid. @@ -27,7 +44,18 @@ import org.fdroid.fdroid.R; */ public class MainActivity extends AppCompatActivity implements BottomNavigationView.OnNavigationItemSelectedListener { + private static final String TAG = "MainActivity"; + + public static final String EXTRA_VIEW_MY_APPS = "org.fdroid.fdroid.views.main.MainActivity.VIEW_MY_APPS"; + + 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 int REQUEST_SWAP = 3; + private RecyclerView pager; + private MainViewAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { @@ -35,13 +63,67 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV setContentView(R.layout.activity_main); + adapter = new MainViewAdapter(this); + pager = (RecyclerView) findViewById(R.id.main_view_pager); pager.setHasFixedSize(true); pager.setLayoutManager(new NonScrollingHorizontalLayoutManager(this)); - pager.setAdapter(new MainViewAdapter(this)); + pager.setAdapter(adapter); BottomNavigationView bottomNavigation = (BottomNavigationView) findViewById(R.id.bottom_navigation); bottomNavigation.setOnNavigationItemSelectedListener(this); + + initialRepoUpdateIfRequired(); + + Intent intent = getIntent(); + handleSearchOrAppViewIntent(intent); + } + + /** + * 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 + * don't try to do it automatically again. + */ + private void initialRepoUpdateIfRequired() { + Preferences prefs = Preferences.get(); + if (!prefs.hasTriedEmptyUpdate()) { + Utils.debugLog(TAG, "We haven't done an update yet. Forcing repo update."); + prefs.setTriedEmptyUpdate(true); + UpdateService.updateNow(this); + } + } + + @Override + protected void onResume() { + super.onResume(); + + FDroidApp.checkStartTor(this); + + if (getIntent().hasExtra(EXTRA_VIEW_MY_APPS)) { + getIntent().removeExtra(EXTRA_VIEW_MY_APPS); + pager.scrollToPosition(adapter.adapterPositionFromItemId(R.id.my_apps)); + } + + // AppDetails 2 and RepoDetailsActivity set different NFC actions, so reset here + NfcHelper.setAndroidBeam(this, getApplication().getPackageName()); + checkForAddRepoIntent(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleSearchOrAppViewIntent(intent); + + // This is called here as well as onResume(), because onNewIntent() is not called the first + // time the activity is created. An alternative option to make sure that the add repo intent + // is always handled is to call setIntent(intent) here. However, after this good read: + // http://stackoverflow.com/a/7749347 it seems that adding a repo is not really more + // important than the original intent which caused the activity to start (even though it + // could technically have been an add repo intent itself). + // The end result is that this method will be called twice for one add repo intent. Once + // here and once in onResume(). However, the method deals with this by ensuring it only + // handles the same intent once. + checkForAddRepoIntent(intent); } @Override @@ -50,6 +132,131 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV return true; } + private void handleSearchOrAppViewIntent(Intent intent) { + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + String query = intent.getStringExtra(SearchManager.QUERY); + performSearch(query); + return; + } + + final Uri data = intent.getData(); + if (data == null) { + return; + } + + final String scheme = data.getScheme(); + final String path = data.getPath(); + String packageName = null; + String query = null; + if (data.isHierarchical()) { + final String host = data.getHost(); + if (host == null) { + return; + } + switch (host) { + case "f-droid.org": + if (path.startsWith("/repository/browse")) { + // http://f-droid.org/repository/browse?fdfilter=search+query + query = UriCompat.getQueryParameter(data, "fdfilter"); + + // http://f-droid.org/repository/browse?fdid=packageName + packageName = UriCompat.getQueryParameter(data, "fdid"); + } else if (path.startsWith("/app")) { + // http://f-droid.org/app/packageName + packageName = data.getLastPathSegment(); + if ("app".equals(packageName)) { + packageName = null; + } + } + break; + case "details": + // market://details?id=app.id + packageName = UriCompat.getQueryParameter(data, "id"); + break; + case "search": + // market://search?q=query + query = UriCompat.getQueryParameter(data, "q"); + break; + case "play.google.com": + if (path.startsWith("/store/apps/details")) { + // http://play.google.com/store/apps/details?id=app.id + packageName = UriCompat.getQueryParameter(data, "id"); + } else if (path.startsWith("/store/search")) { + // http://play.google.com/store/search?q=foo + query = UriCompat.getQueryParameter(data, "q"); + } + break; + case "apps": + case "amazon.com": + case "www.amazon.com": + // amzn://apps/android?p=app.id + // http://amazon.com/gp/mas/dl/android?s=app.id + packageName = UriCompat.getQueryParameter(data, "p"); + query = UriCompat.getQueryParameter(data, "s"); + break; + } + } else if ("fdroid.app".equals(scheme)) { + // fdroid.app:app.id + packageName = data.getSchemeSpecificPart(); + } else if ("fdroid.search".equals(scheme)) { + // fdroid.search:query + query = data.getSchemeSpecificPart(); + } + + if (!TextUtils.isEmpty(query)) { + // an old format for querying via packageName + if (query.startsWith("pname:")) { + packageName = query.split(":")[1]; + } + + // sometimes, search URLs include pub: or other things before the query string + if (query.contains(":")) { + query = query.split(":")[1]; + } + } + + if (!TextUtils.isEmpty(packageName)) { + Utils.debugLog(TAG, "FDroid launched via app link for '" + packageName + "'"); + Intent intentToInvoke = new Intent(this, AppDetails2.class); + intentToInvoke.putExtra(AppDetails.EXTRA_APPID, packageName); + startActivity(intentToInvoke); + finish(); + } else if (!TextUtils.isEmpty(query)) { + Utils.debugLog(TAG, "FDroid launched via search link for '" + query + "'"); + performSearch(query); + } + } + + /** + * Initiates the {@link AppListActivity} with the relevant search terms passed in via the query arg. + */ + private void performSearch(String query) { + Intent searchIntent = new Intent(this, AppListActivity.class); + searchIntent.putExtra(AppListActivity.EXTRA_SEARCH_TERMS, query); + startActivity(searchIntent); + } + + private void checkForAddRepoIntent(Intent intent) { + // Don't handle the intent after coming back to this view (e.g. after hitting the back button) + // http://stackoverflow.com/a/14820849 + if (!intent.hasExtra(ADD_REPO_INTENT_HANDLED)) { + intent.putExtra(ADD_REPO_INTENT_HANDLED, true); + NewRepoConfig parser = new NewRepoConfig(this, intent); + if (parser.isValidRepo()) { + if (parser.isFromSwap()) { + Intent confirmIntent = new Intent(this, SwapWorkflowActivity.class); + confirmIntent.putExtra(SwapWorkflowActivity.EXTRA_CONFIRM, true); + confirmIntent.setData(intent.getData()); + startActivityForResult(confirmIntent, REQUEST_SWAP); + } else { + startActivity(new Intent(ACTION_ADD_REPO, intent.getData(), this, ManageReposActivity.class)); + } + } else if (parser.getErrorMessage() != null) { + Toast.makeText(this, parser.getErrorMessage(), Toast.LENGTH_LONG).show(); + } + } + } + private static class NonScrollingHorizontalLayoutManager extends LinearLayoutManager { NonScrollingHorizontalLayoutManager(Context context) { super(context, LinearLayoutManager.HORIZONTAL, false); diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/MainViewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/main/MainViewAdapter.java index 458bed5bc..7f3762204 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/MainViewAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/MainViewAdapter.java @@ -41,6 +41,30 @@ class MainViewAdapter extends RecyclerView.Adapter { @Override public MainViewController onCreateViewHolder(ViewGroup parent, int viewType) { + MainViewController holder = createEmptyView(); + switch (viewType) { + case R.id.whats_new: + holder.bindWhatsNewView(); + break; + case R.id.categories: + holder.bindCategoriesView(); + break; + case R.id.nearby: + holder.bindSwapView(); + break; + case R.id.my_apps: + holder.bindMyApps(); + break; + case R.id.settings: + holder.bindSettingsView(); + break; + default: + throw new IllegalStateException("Unknown view type " + viewType); + } + return holder; + } + + private MainViewController createEmptyView() { FrameLayout frame = new FrameLayout(activity); frame.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return new MainViewController(activity, frame); @@ -48,20 +72,10 @@ class MainViewAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(MainViewController holder, int position) { - long menuId = getItemId(position); - if (menuId == R.id.whats_new) { - holder.bindWhatsNewView(); - } else if (menuId == R.id.categories) { - holder.bindCategoriesView(); - } else if (menuId == R.id.nearby) { - holder.bindSwapView(); - } else if (menuId == R.id.my_apps) { - holder.bindMyApps(); - } else if (menuId == R.id.settings) { - holder.bindSettingsView(); - } else { - holder.clearViews(); - } + // The binding happens in onCreateViewHolder. This is because we never have more than one of + // each type of view in this main activity. Therefore, there is no benefit to re-binding new + // data each time we navigate back to an item, as the recycler view will just use the one we + // created earlier. } @Override @@ -69,6 +83,11 @@ class MainViewAdapter extends RecyclerView.Adapter { return positionToId.size(); } + @Override + public int getItemViewType(int position) { + return positionToId.get(position); + } + // The RecyclerViewPager and the BottomNavigationView both use menu item IDs to identify pages. @Override public long getItemId(int position) { diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java b/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java index d67ca6f2b..cecced816 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java @@ -30,10 +30,6 @@ class MainViewController extends RecyclerView.ViewHolder { this.frame = frame; } - public void clearViews() { - frame.removeAllViews(); - } - /** * @see WhatsNewViewBinder */ diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/WhatsNewViewBinder.java b/app/src/main/java/org/fdroid/fdroid/views/main/WhatsNewViewBinder.java index 6a01e1233..62cbe6906 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/WhatsNewViewBinder.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/WhatsNewViewBinder.java @@ -1,7 +1,9 @@ package org.fdroid.fdroid.views.main; +import android.content.Intent; import android.database.Cursor; import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; @@ -16,6 +18,7 @@ import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.Schema; +import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.whatsnew.WhatsNewAdapter; /** @@ -52,6 +55,14 @@ class WhatsNewViewBinder implements LoaderManager.LoaderCallbacks { } }); + FloatingActionButton searchFab = (FloatingActionButton) whatsNewView.findViewById(R.id.btn_search); + searchFab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + activity.startActivity(new Intent(activity, AppListActivity.class)); + } + }); + activity.getSupportLoaderManager().initLoader(LOADER_ID, null, this); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index c047b6685..9e2c92a82 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -254,7 +254,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { } private void promptToSetupWifiAP() { - WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); + WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); WifiApControl ap = WifiApControl.getInstance(this); wifiManager.setWifiEnabled(false); if (!ap.enable()) { diff --git a/app/src/main/res/drawable/app_tag_new_background.xml b/app/src/main/res/drawable/app_tag_new_background.xml new file mode 100644 index 000000000..1819a007e --- /dev/null +++ b/app/src/main/res/drawable/app_tag_new_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/donation_option_litecoin.xml b/app/src/main/res/drawable/donation_option_litecoin.xml index 6a7be5275..57deb2328 100644 --- a/app/src/main/res/drawable/donation_option_litecoin.xml +++ b/app/src/main/res/drawable/donation_option_litecoin.xml @@ -1,64 +1,96 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml new file mode 100644 index 000000000..f13f91b85 --- /dev/null +++ b/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_complete.xml b/app/src/main/res/drawable/ic_download_complete.xml new file mode 100644 index 000000000..5074a5848 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_complete.xml @@ -0,0 +1,33 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress.xml b/app/src/main/res/drawable/ic_download_progress.xml new file mode 100644 index 000000000..acc98f429 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_download_progress_0.xml b/app/src/main/res/drawable/ic_download_progress_0.xml new file mode 100644 index 000000000..a5da70a1a --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_0.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_105.xml b/app/src/main/res/drawable/ic_download_progress_105.xml new file mode 100644 index 000000000..321e2a54d --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_105.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_120.xml b/app/src/main/res/drawable/ic_download_progress_120.xml new file mode 100644 index 000000000..1c7a3cbf8 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_120.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_135.xml b/app/src/main/res/drawable/ic_download_progress_135.xml new file mode 100644 index 000000000..898e77257 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_135.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_15.xml b/app/src/main/res/drawable/ic_download_progress_15.xml new file mode 100644 index 000000000..9cadfe39c --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_15.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_150.xml b/app/src/main/res/drawable/ic_download_progress_150.xml new file mode 100644 index 000000000..70c2b1a58 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_150.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_165.xml b/app/src/main/res/drawable/ic_download_progress_165.xml new file mode 100644 index 000000000..06c153384 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_165.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_180.xml b/app/src/main/res/drawable/ic_download_progress_180.xml new file mode 100644 index 000000000..24e201adb --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_180.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_195.xml b/app/src/main/res/drawable/ic_download_progress_195.xml new file mode 100644 index 000000000..c5763fdc5 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_195.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_210.xml b/app/src/main/res/drawable/ic_download_progress_210.xml new file mode 100644 index 000000000..76a3d518b --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_210.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_225.xml b/app/src/main/res/drawable/ic_download_progress_225.xml new file mode 100644 index 000000000..d9dd70ef4 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_225.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_240.xml b/app/src/main/res/drawable/ic_download_progress_240.xml new file mode 100644 index 000000000..36cfb98be --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_240.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_255.xml b/app/src/main/res/drawable/ic_download_progress_255.xml new file mode 100644 index 000000000..f6ec04e95 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_255.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_270.xml b/app/src/main/res/drawable/ic_download_progress_270.xml new file mode 100644 index 000000000..27ea33ca6 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_270.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_285.xml b/app/src/main/res/drawable/ic_download_progress_285.xml new file mode 100644 index 000000000..40546caa2 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_285.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_30.xml b/app/src/main/res/drawable/ic_download_progress_30.xml new file mode 100644 index 000000000..bb7984e51 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_30.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_300.xml b/app/src/main/res/drawable/ic_download_progress_300.xml new file mode 100644 index 000000000..fbc481142 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_300.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_315.xml b/app/src/main/res/drawable/ic_download_progress_315.xml new file mode 100644 index 000000000..7f089ee0e --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_315.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_330.xml b/app/src/main/res/drawable/ic_download_progress_330.xml new file mode 100644 index 000000000..2f76d525b --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_330.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_345.xml b/app/src/main/res/drawable/ic_download_progress_345.xml new file mode 100644 index 000000000..5e95469ca --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_345.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_360.xml b/app/src/main/res/drawable/ic_download_progress_360.xml new file mode 100644 index 000000000..53eb56bd7 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_360.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_45.xml b/app/src/main/res/drawable/ic_download_progress_45.xml new file mode 100644 index 000000000..8e16b0e89 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_45.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_60.xml b/app/src/main/res/drawable/ic_download_progress_60.xml new file mode 100644 index 000000000..8870e9fd8 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_60.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_75.xml b/app/src/main/res/drawable/ic_download_progress_75.xml new file mode 100644 index 000000000..8a9f96dc5 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_75.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_progress_90.xml b/app/src/main/res/drawable/ic_download_progress_90.xml new file mode 100644 index 000000000..4f9a2e3ef --- /dev/null +++ b/app/src/main/res/drawable/ic_download_progress_90.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/app/src/main/res/layout-v14/app_status_new.xml b/app/src/main/res/layout-v14/app_status_new.xml new file mode 100644 index 000000000..0b0e68f04 --- /dev/null +++ b/app/src/main/res/layout-v14/app_status_new.xml @@ -0,0 +1,17 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_app_list.xml b/app/src/main/res/layout/activity_app_list.xml index f81d7ba9e..0e3f2969b 100644 --- a/app/src/main/res/layout/activity_app_list.xml +++ b/app/src/main/res/layout/activity_app_list.xml @@ -30,7 +30,7 @@ app:srcCompat="@drawable/ic_back_black_24dp" /> diff --git a/app/src/main/res/layout/app_card_featured.xml b/app/src/main/res/layout/app_card_featured.xml index 9f1da484d..d74565aac 100644 --- a/app/src/main/res/layout/app_card_featured.xml +++ b/app/src/main/res/layout/app_card_featured.xml @@ -7,7 +7,7 @@ android:paddingBottom="2dp" android:clipToPadding="false"> - + + app:layout_constraintTop_toTopOf="parent" + tools:ignore="ContentDescription" /> - diff --git a/app/src/main/res/layout/app_card_horizontal.xml b/app/src/main/res/layout/app_card_horizontal.xml index 9baf5f7f0..3fa5c99de 100644 --- a/app/src/main/res/layout/app_card_horizontal.xml +++ b/app/src/main/res/layout/app_card_horizontal.xml @@ -10,22 +10,24 @@ android:layout_height="wrap_content" android:layout_margin="8dp"> + + app:layout_constraintTop_toTopOf="parent" + tools:ignore="ContentDescription" /> - + app:layout_constraintStart_toStartOf="@+id/summary" + app:layout_constraintLeft_toLeftOf="@+id/summary" /> diff --git a/app/src/main/res/layout/app_card_large.xml b/app/src/main/res/layout/app_card_large.xml index df691a2ff..f0ea45430 100644 --- a/app/src/main/res/layout/app_card_large.xml +++ b/app/src/main/res/layout/app_card_large.xml @@ -10,9 +10,10 @@ android:layout_height="match_parent" android:layout_margin="8dp"> + + app:layout_constraintTop_toTopOf="parent" + tools:ignore="ContentDescription" /> - + android:id="@+id/new_tag" + android:layout_marginTop="4dp" + app:layout_constraintTop_toBottomOf="@+id/summary" + app:layout_constraintStart_toStartOf="@+id/summary" + app:layout_constraintLeft_toLeftOf="@+id/summary" /> diff --git a/app/src/main/res/layout/app_card_list_item.xml b/app/src/main/res/layout/app_card_list_item.xml index 5373d6cd4..4c550ee1a 100644 --- a/app/src/main/res/layout/app_card_list_item.xml +++ b/app/src/main/res/layout/app_card_list_item.xml @@ -10,15 +10,17 @@ android:layout_height="match_parent" android:layout_margin="8dp"> + + app:layout_constraintTop_toTopOf="parent" + tools:ignore="ContentDescription" /> - + app:layout_constraintStart_toStartOf="@+id/summary" + app:layout_constraintLeft_toLeftOf="@+id/summary" /> diff --git a/app/src/main/res/layout/app_details2.xml b/app/src/main/res/layout/app_details2.xml index 31b441b7e..4e7316d0a 100644 --- a/app/src/main/res/layout/app_details2.xml +++ b/app/src/main/res/layout/app_details2.xml @@ -25,13 +25,13 @@ app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed"> - + + android:layout_marginTop="8dp" + tools:ignore="ContentDescription" /> -