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