Merge branch 'app_details_749' into 'master'

App details 749

The new App Details screen (issue #749), now accessible by long-clicking on an app in the application list.

See merge request !419
This commit is contained in:
Peter Serwylo 2016-12-08 01:19:49 +00:00
commit f9f0a0f91c
49 changed files with 2776 additions and 76 deletions

View File

@ -21,6 +21,9 @@ dependencies {
compile 'com.android.support:support-v4:24.2.1'
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support:support-annotations:24.2.1'
compile 'com.android.support:design:24.2.1'
compile 'com.android.support:cardview-v7:24.2.1'
compile "com.android.support:recyclerview-v7:24.2.1"
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
compile 'com.google.zxing:core:3.2.1'
@ -41,6 +44,9 @@ dependencies {
testCompile "org.robolectric:robolectric:3.1.2"
// As per https://github.com/robolectric/robolectric/issues/1932#issuecomment-219796474
testCompile 'org.khronos:opengl-api:gl1.1-android-2.1_r1'
testCompile "org.mockito:mockito-core:1.10.19"
androidTestCompile 'com.android.support:support-annotations:24.2.1'
@ -97,6 +103,9 @@ if (!hasProperty('sourceDeps')) {
'com.android.support:support-media-compat:fa29a23eadd685631584b2c0c624a36e3bb79a33e257b00304501ad682fa2be3',
'com.android.support:support-v4:cac2956f5c4bb363cc0ba824ac16ea2a687d1c305d434416a34772a5f9375ed7',
'com.android.support:support-vector-drawable:6ee37a7f7b93c1df1294e6f6f97df3724ac989fcda0549faf677001085330548',
'com.android.support:design:89842bb1243507fe3079066ea4ea58795effe69cdf9a819e05274d21760adfc2',
'com.android.support:cardview-v7:2303b351686d1db060b5fcf1a9c709c79b4a54a85bfda0fb3c4849e244606ee1',
'com.android.support:recyclerview-v7:9077766a1a0f4e89528fbf9dcdf6d5880a8686f0266fa852d58d803beeef18fa',
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
'com.madgag.spongycastle:core:9b6b7ac856b91bcda2ede694eccd26cefb0bf0b09b89f13cda05b5da5ff68c6b',
'com.madgag.spongycastle:pkix:6aba9b2210907a3d46dd3dcac782bb3424185290468d102d5207ebdc9796a905',

View File

@ -404,6 +404,17 @@
android:value=".FDroid" />
</activity>
<activity
android:name=".AppDetails2"
android:label="@string/app_details"
android:exported="true"
android:parentActivityName=".FDroid"
android:theme="@style/AppThemeLight.NoActionBar"
android:configChanges="layoutDirection|locale" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".FDroid" />
</activity>
<activity
android:label="@string/menu_settings"
android:name=".PreferencesActivity"

View File

@ -158,7 +158,7 @@ public class AppDetails extends AppCompatActivity {
class ApkListAdapter extends ArrayAdapter<Apk> {
private final LayoutInflater mInflater = (LayoutInflater) context.getSystemService(
private final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
ApkListAdapter(Context context, App app) {
@ -204,7 +204,7 @@ public class AppDetails extends AppCompatActivity {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.apklistitem, parent, false);
convertView = inflater.inflate(R.layout.apklistitem, parent, false);
holder = new ViewHolder();
holder.version = (TextView) convertView.findViewById(R.id.version);
@ -1157,7 +1157,7 @@ public class AppDetails extends AppCompatActivity {
}
};
private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
private final View.OnClickListener onClickListener = new View.OnClickListener() {
public void onClick(View v) {
String url = null;
App app = appDetails.getApp();
@ -1182,13 +1182,13 @@ public class AppDetails extends AppCompatActivity {
url = app.donateURL;
break;
case R.id.bitcoin:
url = "bitcoin:" + app.bitcoinAddr;
url = app.getBitcoinUri();
break;
case R.id.litecoin:
url = "litecoin:" + app.litecoinAddr;
url = app.getLitecoinUri();
break;
case R.id.flattr:
url = "https://flattr.com/thing/" + app.flattrID;
url = app.getFlattrUri();
break;
}
if (url != null) {
@ -1268,7 +1268,7 @@ public class AppDetails extends AppCompatActivity {
// Website button
View tv = view.findViewById(R.id.website);
if (!TextUtils.isEmpty(app.webURL)) {
tv.setOnClickListener(mOnClickListener);
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@ -1276,7 +1276,7 @@ public class AppDetails extends AppCompatActivity {
// Email button
tv = view.findViewById(R.id.email);
if (!TextUtils.isEmpty(app.email)) {
tv.setOnClickListener(mOnClickListener);
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@ -1284,7 +1284,7 @@ public class AppDetails extends AppCompatActivity {
// Source button
tv = view.findViewById(R.id.source);
if (!TextUtils.isEmpty(app.sourceURL)) {
tv.setOnClickListener(mOnClickListener);
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@ -1292,7 +1292,7 @@ public class AppDetails extends AppCompatActivity {
// Issues button
tv = view.findViewById(R.id.issues);
if (!TextUtils.isEmpty(app.trackerURL)) {
tv.setOnClickListener(mOnClickListener);
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@ -1300,7 +1300,7 @@ public class AppDetails extends AppCompatActivity {
// Changelog button
tv = view.findViewById(R.id.changelog);
if (!TextUtils.isEmpty(app.changelogURL)) {
tv.setOnClickListener(mOnClickListener);
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@ -1308,7 +1308,7 @@ public class AppDetails extends AppCompatActivity {
// Donate button
tv = view.findViewById(R.id.donate);
if (!TextUtils.isEmpty(app.donateURL)) {
tv.setOnClickListener(mOnClickListener);
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@ -1316,7 +1316,7 @@ public class AppDetails extends AppCompatActivity {
// Bitcoin
tv = view.findViewById(R.id.bitcoin);
if (!TextUtils.isEmpty(app.bitcoinAddr)) {
tv.setOnClickListener(mOnClickListener);
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@ -1324,7 +1324,7 @@ public class AppDetails extends AppCompatActivity {
// Litecoin
tv = view.findViewById(R.id.litecoin);
if (!TextUtils.isEmpty(app.litecoinAddr)) {
tv.setOnClickListener(mOnClickListener);
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@ -1332,7 +1332,7 @@ public class AppDetails extends AppCompatActivity {
// Flattr
tv = view.findViewById(R.id.flattr);
if (!TextUtils.isEmpty(app.flattrID)) {
tv.setOnClickListener(mOnClickListener);
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@ -1620,7 +1620,7 @@ public class AppDetails extends AppCompatActivity {
NfcHelper.disableAndroidBeam(appDetails);
// Set Install button and hide second button
btMain.setText(R.string.menu_install);
btMain.setOnClickListener(mOnClickListener);
btMain.setOnClickListener(onClickListener);
btMain.setEnabled(true);
} else if (app.isInstalled()) {
// If App is installed
@ -1638,7 +1638,7 @@ public class AppDetails extends AppCompatActivity {
btMain.setText(R.string.menu_uninstall);
}
}
btMain.setOnClickListener(mOnClickListener);
btMain.setOnClickListener(onClickListener);
btMain.setEnabled(true);
}
TextView author = (TextView) view.findViewById(R.id.author);
@ -1656,7 +1656,7 @@ public class AppDetails extends AppCompatActivity {
}
private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
private final View.OnClickListener onClickListener = new View.OnClickListener() {
public void onClick(View v) {
App app = appDetails.getApp();
AppDetails activity = (AppDetails) getActivity();

View File

@ -0,0 +1,575 @@
package org.fdroid.fdroid;
import android.app.Activity;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
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.app.AppCompatDelegate;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ImageView;
import android.widget.Toast;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppPrefsProvider;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.InstallerFactory;
import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService;
import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter;
import org.fdroid.fdroid.views.ShareChooserDialog;
public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog.ShareChooserDialogListener, AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks {
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
private static final String TAG = "AppDetails2";
private static final int REQUEST_ENABLE_BLUETOOTH = 2;
private static final int REQUEST_PERMISSION_DIALOG = 3;
private static final int REQUEST_UNINSTALL_DIALOG = 4;
private FDroidApp fdroidApp;
private App app;
private RecyclerView recyclerView;
private AppDetailsRecyclerViewAdapter adapter;
private LocalBroadcastManager localBroadcastManager;
private String activeDownloadUrlString;
@Override
protected void onCreate(Bundle savedInstanceState) {
fdroidApp = (FDroidApp) getApplication();
//fdroidApp.applyTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.app_details2);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.setTitle(""); // Nice and clean toolbar
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (!reset(getPackageNameFromIntent(getIntent()))) {
finish();
return;
}
localBroadcastManager = LocalBroadcastManager.getInstance(this);
recyclerView = (RecyclerView) findViewById(R.id.rvDetails);
adapter = new AppDetailsRecyclerViewAdapter(this, app, this);
LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
lm.setStackFromEnd(false);
recyclerView.setLayoutManager(lm);
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);
}
}
private String getPackageNameFromIntent(Intent intent) {
if (!intent.hasExtra(AppDetails.EXTRA_APPID)) {
Log.e(TAG, "No package name found in the intent!");
return null;
}
return intent.getStringExtra(AppDetails.EXTRA_APPID);
}
/**
* If passed null, this will show a message to the user ("Could not find app ..." or something
* like that) and then finish the activity.
*/
private void setApp(App newApp) {
if (newApp == null) {
Toast.makeText(this, R.string.no_such_app, Toast.LENGTH_LONG).show();
finish();
return;
}
app = newApp;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
boolean ret = super.onCreateOptionsMenu(menu);
if (ret) {
getMenuInflater().inflate(R.menu.details2, menu);
}
return ret;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
if (app == null) {
return true;
}
MenuItem itemIgnoreAll = menu.findItem(R.id.action_ignore_all);
if (itemIgnoreAll != null) {
itemIgnoreAll.setChecked(app.getPrefs(this).ignoreAllUpdates);
}
MenuItem itemIgnoreThis = menu.findItem(R.id.action_ignore_this);
if (itemIgnoreThis != null) {
itemIgnoreThis.setVisible(app.hasUpdates());
itemIgnoreThis.setChecked(app.getPrefs(this).ignoreThisUpdate >= app.suggestedVersionCode);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_share) {
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT, app.name);
shareIntent.putExtra(Intent.EXTRA_TEXT, app.name + " (" + app.summary + ") - https://f-droid.org/app/" + app.packageName);
boolean showNearbyItem = app.isInstalled() && fdroidApp.bluetoothAdapter != null;
ShareChooserDialog.createChooser((CoordinatorLayout) findViewById(R.id.rootCoordinator), this, this, shareIntent, showNearbyItem);
return true;
} else if (item.getItemId() == R.id.action_ignore_all) {
app.getPrefs(this).ignoreAllUpdates ^= true;
item.setChecked(app.getPrefs(this).ignoreAllUpdates);
AppPrefsProvider.Helper.update(this, app, app.getPrefs(this));
return true;
} else if (item.getItemId() == R.id.action_ignore_this) {
if (app.getPrefs(this).ignoreThisUpdate >= app.suggestedVersionCode) {
app.getPrefs(this).ignoreThisUpdate = 0;
} else {
app.getPrefs(this).ignoreThisUpdate = app.suggestedVersionCode;
}
item.setChecked(app.getPrefs(this).ignoreThisUpdate > 0);
AppPrefsProvider.Helper.update(this, app, app.getPrefs(this));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onNearby() {
// If Bluetooth has not been enabled/turned on, then
// enabling device discoverability will automatically enable Bluetooth
Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverBt.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 121);
startActivityForResult(discoverBt, REQUEST_ENABLE_BLUETOOTH);
// if this is successful, the Bluetooth transfer is started
}
@Override
public void onResolvedShareIntent(Intent shareIntent) {
startActivity(shareIntent);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_ENABLE_BLUETOOTH:
fdroidApp.sendViaBluetooth(this, resultCode, app.packageName);
break;
case REQUEST_PERMISSION_DIALOG:
if (resultCode == Activity.RESULT_OK) {
Uri uri = data.getData();
Apk apk = ApkProvider.Helper.findByUri(this, uri, Schema.ApkTable.Cols.ALL);
startInstall(apk);
}
break;
case REQUEST_UNINSTALL_DIALOG:
if (resultCode == Activity.RESULT_OK) {
startUninstall();
}
break;
}
}
// Install the version of this app denoted by 'app.curApk'.
@Override
public void installApk(final Apk apk) {
if (isFinishing()) {
return;
}
if (!apk.compatible) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.installIncompatible);
builder.setPositiveButton(R.string.yes,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
initiateInstall(apk);
}
});
builder.setNegativeButton(R.string.no,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
}
});
AlertDialog alert = builder.create();
alert.show();
return;
}
if (app.installedSig != null && apk.sig != null
&& !apk.sig.equals(app.installedSig)) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.SignatureMismatch).setPositiveButton(
R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
AlertDialog alert = builder.create();
alert.show();
return;
}
initiateInstall(apk);
}
private void initiateInstall(Apk apk) {
Installer installer = InstallerFactory.create(this, apk);
Intent intent = installer.getPermissionScreen();
if (intent != null) {
// permission screen required
Utils.debugLog(TAG, "permission screen required");
startActivityForResult(intent, REQUEST_PERMISSION_DIALOG);
return;
}
startInstall(apk);
}
private void startInstall(Apk apk) {
activeDownloadUrlString = apk.getUrl();
registerDownloaderReceiver();
InstallManagerService.queue(this, app, apk);
}
private void startUninstall() {
registerUninstallReceiver();
InstallerService.uninstall(this, app.installedApk);
}
private void registerUninstallReceiver() {
localBroadcastManager.registerReceiver(uninstallReceiver,
Installer.getUninstallIntentFilter(app.packageName));
}
private void unregisterUninstallReceiver() {
localBroadcastManager.unregisterReceiver(uninstallReceiver);
}
private void registerDownloaderReceiver() {
if (activeDownloadUrlString != null) { // if a download is active
String url = activeDownloadUrlString;
localBroadcastManager.registerReceiver(downloadReceiver,
DownloaderService.getIntentFilter(url));
}
}
private void unregisterDownloaderReceiver() {
localBroadcastManager.unregisterReceiver(downloadReceiver);
}
private void unregisterInstallReceiver() {
localBroadcastManager.unregisterReceiver(installReceiver);
}
private final BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Downloader.ACTION_STARTED:
adapter.setProgress(-1, -1, R.string.download_pending);
break;
case Downloader.ACTION_PROGRESS:
adapter.setProgress(intent.getIntExtra(Downloader.EXTRA_BYTES_READ, -1),
intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, -1), 0);
break;
case Downloader.ACTION_COMPLETE:
// Starts the install process once the download is complete.
cleanUpFinishedDownload();
localBroadcastManager.registerReceiver(installReceiver,
Installer.getInstallIntentFilter(intent.getData()));
break;
case Downloader.ACTION_INTERRUPTED:
if (intent.hasExtra(Downloader.EXTRA_ERROR_MESSAGE)) {
String msg = intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)
+ " " + intent.getDataString();
Toast.makeText(context, R.string.download_error, Toast.LENGTH_SHORT).show();
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
} else { // user canceled
Toast.makeText(context, R.string.details_notinstalled, Toast.LENGTH_LONG).show();
}
cleanUpFinishedDownload();
break;
default:
throw new RuntimeException("intent action not handled!");
}
}
};
private final BroadcastReceiver installReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Installer.ACTION_INSTALL_STARTED:
adapter.setProgress(-1, -1, R.string.installing);
break;
case Installer.ACTION_INSTALL_COMPLETE:
adapter.clearProgress();
unregisterInstallReceiver();
onAppChanged();
break;
case Installer.ACTION_INSTALL_INTERRUPTED:
adapter.clearProgress();
onAppChanged();
String errorMessage =
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
if (!TextUtils.isEmpty(errorMessage)) {
Log.e(TAG, "install aborted with errorMessage: " + errorMessage);
String title = String.format(
getString(R.string.install_error_notify_title),
app.name);
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails2.this);
alertBuilder.setTitle(title);
alertBuilder.setMessage(errorMessage);
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
unregisterInstallReceiver();
break;
case Installer.ACTION_INSTALL_USER_INTERACTION:
PendingIntent installPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
installPendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "PI canceled", e);
}
break;
default:
throw new RuntimeException("intent action not handled!");
}
}
};
private final BroadcastReceiver uninstallReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Installer.ACTION_UNINSTALL_STARTED:
adapter.setProgress(-1, -1, R.string.uninstalling);
break;
case Installer.ACTION_UNINSTALL_COMPLETE:
adapter.clearProgress();
onAppChanged();
unregisterUninstallReceiver();
break;
case Installer.ACTION_UNINSTALL_INTERRUPTED:
adapter.clearProgress();
String errorMessage =
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
if (!TextUtils.isEmpty(errorMessage)) {
Log.e(TAG, "uninstall aborted with errorMessage: " + errorMessage);
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails2.this);
alertBuilder.setTitle(R.string.uninstall_error_notify_title);
alertBuilder.setMessage(errorMessage);
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
unregisterUninstallReceiver();
break;
case Installer.ACTION_UNINSTALL_USER_INTERACTION:
PendingIntent uninstallPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
uninstallPendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "PI canceled", e);
}
break;
default:
throw new RuntimeException("intent action not handled!");
}
}
};
/**
* Reset the display and list contents. Used when entering the activity, and
* also when something has been installed/uninstalled.
* Return true if the app was found, false otherwise.
*/
private boolean reset(String packageName) {
Utils.debugLog(TAG, "Getting application details for " + packageName);
App newApp = null;
calcActiveDownloadUrlString(packageName);
if (!TextUtils.isEmpty(packageName)) {
newApp = AppProvider.Helper.findHighestPriorityMetadata(getContentResolver(), packageName);
}
setApp(newApp);
return this.app != null;
}
private void calcActiveDownloadUrlString(String packageName) {
String urlString = getPreferences(MODE_PRIVATE).getString(packageName, null);
if (DownloaderService.isQueuedOrActive(urlString)) {
activeDownloadUrlString = urlString;
} else {
// this URL is no longer active, remove it
getPreferences(MODE_PRIVATE).edit().remove(packageName).apply();
}
}
/**
* Remove progress listener, suppress progress bar, set downloadHandler to null.
*/
private void cleanUpFinishedDownload() {
activeDownloadUrlString = null;
adapter.clearProgress();
unregisterDownloaderReceiver();
}
private void onAppChanged() {
recyclerView.post(new Runnable() {
@Override
public void run() {
if (!reset(app.packageName)) {
AppDetails2.this.finish();
return;
}
AppDetailsRecyclerViewAdapter adapter = (AppDetailsRecyclerViewAdapter) recyclerView.getAdapter();
adapter.updateItems(app);
supportInvalidateOptionsMenu();
}
});
}
@Override
public boolean isAppDownloading() {
return !TextUtils.isEmpty(activeDownloadUrlString);
}
@Override
public void enableAndroidBeam() {
NfcHelper.setAndroidBeam(this, app.packageName);
}
@Override
public void disableAndroidBeam() {
NfcHelper.disableAndroidBeam(this);
}
@Override
public void openUrl(String url) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
if (intent.resolveActivity(getPackageManager()) == null) {
Toast.makeText(this,
getString(R.string.no_handler_app, intent.getDataString()),
Toast.LENGTH_LONG).show();
return;
}
startActivity(intent);
}
@Override
public void installCancel() {
if (!TextUtils.isEmpty(activeDownloadUrlString)) {
InstallManagerService.cancel(this, activeDownloadUrlString);
}
}
@Override
public void launchApk() {
Intent intent = getPackageManager().getLaunchIntentForPackage(app.packageName);
startActivity(intent);
}
@Override
public void installApk() {
Apk apkToInstall = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode);
installApk(apkToInstall);
}
@Override
public void upgradeApk() {
Apk apkToInstall = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode);
installApk(apkToInstall);
}
@Override
public void uninstallApk() {
Apk apk = app.installedApk;
if (apk == null) {
// TODO ideally, app would be refreshed immediately after install, then this
// workaround would be unnecessary
try {
PackageInfo pi = getPackageManager().getPackageInfo(app.packageName, 0);
apk = ApkProvider.Helper.findApkFromAnyRepo(this, pi.packageName, pi.versionCode);
app.installedApk = apk;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return; // not installed
}
}
Installer installer = InstallerFactory.create(this, apk);
Intent intent = installer.getUninstallScreen();
if (intent != null) {
// uninstall screen required
Utils.debugLog(TAG, "screen screen required");
startActivityForResult(intent, REQUEST_UNINSTALL_DIALOG);
return;
}
startUninstall();
}
}

View File

@ -20,6 +20,7 @@ package org.fdroid.fdroid;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
@ -31,6 +32,7 @@ import android.text.Html;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
@ -596,4 +598,8 @@ public final class Utils {
return data;
}
public static int dpToPx(int dp, Context ctx) {
Resources r = ctx.getResources();
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics());
}
}

View File

@ -202,7 +202,7 @@ public final class PRNGFixes {
* each instance needs to seed itself if the client does not explicitly
* seed it.
*/
private boolean mSeeded;
private boolean seeded;
@Override
protected void engineSetSeed(byte[] bytes) {
@ -218,13 +218,13 @@ public final class PRNGFixes {
// Log and ignore.
Log.w(TAG, "Failed to mix seed into " + URANDOM_FILE);
} finally {
mSeeded = true;
seeded = true;
}
}
@Override
protected void engineNextBytes(byte[] bytes) {
if (!mSeeded) {
if (!seeded) {
// Mix in the device- and invocation-specific seed.
engineSetSeed(generateSeed());
}

View File

@ -12,6 +12,7 @@ import android.database.Cursor;
import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
@ -552,6 +553,21 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
return new AppFilter().filter(this);
}
@Nullable
public String getBitcoinUri() {
return TextUtils.isEmpty(bitcoinAddr) ? null : "bitcoin:" + bitcoinAddr;
}
@Nullable
public String getLitecoinUri() {
return TextUtils.isEmpty(litecoinAddr) ? null : "litecoin:" + litecoinAddr;
}
@Nullable
public String getFlattrUri() {
return TextUtils.isEmpty(flattrID) ? null : "https://flattr.com/thing/" + flattrID;
}
public String getSuggestedVersionName() {
return suggestedVersionName;
}

View File

@ -46,7 +46,7 @@ final class BonjourFinder extends PeerFinder implements ServiceListener {
private JmDNS jmdns;
private WifiManager wifiManager;
private WifiManager.MulticastLock mMulticastLock;
private WifiManager.MulticastLock multicastLock;
private BonjourFinder(Context context, Subscriber<? super Peer> subscriber) {
super(context, subscriber);
@ -58,8 +58,8 @@ final class BonjourFinder extends PeerFinder implements ServiceListener {
if (wifiManager == null) {
wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
mMulticastLock = wifiManager.createMulticastLock(context.getPackageName());
mMulticastLock.setReferenceCounted(false);
multicastLock = wifiManager.createMulticastLock(context.getPackageName());
multicastLock.setReferenceCounted(false);
}
if (isScanning) {
@ -68,7 +68,7 @@ final class BonjourFinder extends PeerFinder implements ServiceListener {
}
isScanning = true;
mMulticastLock.acquire();
multicastLock.acquire();
try {
Utils.debugLog(TAG, "Searching for Bonjour (mDNS) clients...");
@ -146,8 +146,8 @@ final class BonjourFinder extends PeerFinder implements ServiceListener {
private void cancel() {
Utils.debugLog(TAG, "Cancelling BonjourFinder, releasing multicast lock, removing jmdns service listeners");
if (mMulticastLock != null) {
mMulticastLock.release();
if (multicastLock != null) {
multicastLock.release();
}
isScanning = false;

View File

@ -129,7 +129,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
* 1. Check for root access
*/
private final AsyncTask<Void, Void, Boolean> checkRootTask = new AsyncTask<Void, Void, Boolean>() {
ProgressDialog mProgressDialog;
ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
@ -139,11 +139,11 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
ContextThemeWrapper theme = new ContextThemeWrapper(InstallExtensionDialogActivity.this,
FDroidApp.getCurThemeResId());
mProgressDialog = new ProgressDialog(theme);
mProgressDialog.setMessage(getString(R.string.requesting_root_access_body));
mProgressDialog.setIndeterminate(true);
mProgressDialog.setCancelable(false);
mProgressDialog.show();
progressDialog = new ProgressDialog(theme);
progressDialog.setMessage(getString(R.string.requesting_root_access_body));
progressDialog.setIndeterminate(true);
progressDialog.setCancelable(false);
progressDialog.show();
}
@Override
@ -155,7 +155,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
protected void onPostExecute(Boolean rootGranted) {
super.onPostExecute(rootGranted);
mProgressDialog.dismiss();
progressDialog.dismiss();
if (rootGranted) {
// root access granted
@ -193,7 +193,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
* 2. Install into system
*/
private final AsyncTask<Void, Void, Void> installTask = new AsyncTask<Void, Void, Void>() {
ProgressDialog mProgressDialog;
ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
@ -203,11 +203,11 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
ContextThemeWrapper theme = new ContextThemeWrapper(InstallExtensionDialogActivity.this,
FDroidApp.getCurThemeResId());
mProgressDialog = new ProgressDialog(theme);
mProgressDialog.setMessage(InstallExtension.create(getApplicationContext()).getInstallingString());
mProgressDialog.setIndeterminate(true);
mProgressDialog.setCancelable(false);
mProgressDialog.show();
progressDialog = new ProgressDialog(theme);
progressDialog.setMessage(InstallExtension.create(getApplicationContext()).getInstallingString());
progressDialog.setIndeterminate(true);
progressDialog.setCancelable(false);
progressDialog.show();
}
@Override
@ -298,7 +298,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
}
private final AsyncTask<Void, Void, Void> uninstallTask = new AsyncTask<Void, Void, Void>() {
ProgressDialog mProgressDialog;
ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
@ -308,11 +308,11 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
ContextThemeWrapper theme = new ContextThemeWrapper(InstallExtensionDialogActivity.this,
FDroidApp.getCurThemeResId());
mProgressDialog = new ProgressDialog(theme);
mProgressDialog.setMessage(getString(R.string.system_install_uninstalling));
mProgressDialog.setIndeterminate(true);
mProgressDialog.setCancelable(false);
mProgressDialog.show();
progressDialog = new ProgressDialog(theme);
progressDialog.setMessage(getString(R.string.system_install_uninstalling));
progressDialog.setIndeterminate(true);
progressDialog.setCancelable(false);
progressDialog.show();
}
@Override
@ -325,7 +325,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
protected void onPostExecute(Void unused) {
super.onPostExecute(unused);
mProgressDialog.dismiss();
progressDialog.dismiss();
// app is uninstalled but still display, kill it!
System.exit(0);

View File

@ -90,7 +90,7 @@ public class AppSecurityPermissions {
// PermissionGroupInfo implements Parcelable but its Parcel constructor is private and thus cannot be extended.
@SuppressLint("ParcelCreator")
static class MyPermissionGroupInfo extends PermissionGroupInfo {
CharSequence mLabel;
CharSequence label;
final List<MyPermissionInfo> newPermissions = new ArrayList<>();
final List<MyPermissionInfo> allPermissions = new ArrayList<>();
@ -185,7 +185,7 @@ public class AppSecurityPermissions {
}
PackageManager pm = getContext().getPackageManager();
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle(group.mLabel);
builder.setTitle(group.label);
if (perm.descriptionRes != 0) {
builder.setMessage(perm.loadDescription(pm));
} else {
@ -431,22 +431,22 @@ public class AppSecurityPermissions {
private static class PermissionGroupInfoComparator implements Comparator<MyPermissionGroupInfo> {
private final Collator sCollator = Collator.getInstance();
private final Collator collator = Collator.getInstance();
public final int compare(MyPermissionGroupInfo a, MyPermissionGroupInfo b) {
return sCollator.compare(a.mLabel, b.mLabel);
return collator.compare(a.label, b.label);
}
}
private static class PermissionInfoComparator implements Comparator<MyPermissionInfo> {
private final Collator sCollator = Collator.getInstance();
private final Collator collator = Collator.getInstance();
PermissionInfoComparator() {
}
public final int compare(MyPermissionInfo a, MyPermissionInfo b) {
return sCollator.compare(a.label, b.label);
return collator.compare(a.label, b.label);
}
}
@ -482,13 +482,13 @@ public class AppSecurityPermissions {
for (MyPermissionGroupInfo pgrp : permGroups.values()) {
if (pgrp.labelRes != 0 || pgrp.nonLocalizedLabel != null) {
pgrp.mLabel = pgrp.loadLabel(pm);
pgrp.label = pgrp.loadLabel(pm);
} else {
try {
ApplicationInfo app = pm.getApplicationInfo(pgrp.packageName, 0);
pgrp.mLabel = app.loadLabel(pm);
pgrp.label = app.loadLabel(pm);
} catch (NameNotFoundException e) {
pgrp.mLabel = pgrp.loadLabel(pm);
pgrp.label = pgrp.loadLabel(pm);
}
}
permGroupsList.add(pgrp);

View File

@ -0,0 +1,881 @@
package org.fdroid.fdroid.views;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.TextViewCompat;
import android.support.v7.text.AllCapsTransformationMethod;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Html;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import org.fdroid.fdroid.Preferences;
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.InstalledAppProvider;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.privileged.views.AppDiff;
import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
public class AppDetailsRecyclerViewAdapter
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public interface AppDetailsRecyclerViewAdapterCallbacks {
boolean isAppDownloading();
void enableAndroidBeam();
void disableAndroidBeam();
void openUrl(String url);
void installApk();
void installApk(Apk apk);
void upgradeApk();
void uninstallApk();
void installCancel();
void launchApk();
}
private static final int VIEWTYPE_HEADER = 0;
private static final int VIEWTYPE_SCREENSHOTS = 1;
private static final int VIEWTYPE_WHATS_NEW = 2;
private static final int VIEWTYPE_DONATE = 3;
private static final int VIEWTYPE_LINKS = 4;
private static final int VIEWTYPE_PERMISSIONS = 5;
private static final int VIEWTYPE_VERSIONS = 6;
private static final int VIEWTYPE_VERSION = 7;
private final Context context;
@NonNull
private App app;
private final AppDetailsRecyclerViewAdapterCallbacks callbacks;
private RecyclerView recyclerView;
private ArrayList<Object> items;
private ArrayList<Apk> versions;
private boolean showVersions;
private HeaderViewHolder headerView;
public AppDetailsRecyclerViewAdapter(Context context, @NonNull App app, AppDetailsRecyclerViewAdapterCallbacks callbacks) {
this.context = context;
this.callbacks = callbacks;
this.app = app;
updateItems(app);
}
public void updateItems(@NonNull App app) {
this.app = app;
// Get versions
versions = new ArrayList<>();
final List<Apk> apks = ApkProvider.Helper.findByPackageName(context, this.app.packageName);
for (final Apk apk : apks) {
if (apk.compatible || Preferences.get().showIncompatibleVersions()) {
versions.add(apk);
}
}
if (items == null) {
items = new ArrayList<>();
} else {
items.clear();
}
addItem(VIEWTYPE_HEADER);
addItem(VIEWTYPE_SCREENSHOTS);
addItem(VIEWTYPE_WHATS_NEW);
addItem(VIEWTYPE_DONATE);
addItem(VIEWTYPE_LINKS);
addItem(VIEWTYPE_PERMISSIONS);
addItem(VIEWTYPE_VERSIONS);
notifyDataSetChanged();
}
private void setShowVersions(boolean showVersions) {
this.showVersions = showVersions;
items.removeAll(versions);
if (showVersions) {
items.addAll(items.indexOf(VIEWTYPE_VERSIONS) + 1, versions);
}
notifyDataSetChanged();
}
private void addItem(int item) {
// Gives us a chance to hide sections that are not used, e.g. the donate section when
// we have no donation links.
if (item == VIEWTYPE_DONATE && !shouldShowDonate()) {
return;
} else if (item == VIEWTYPE_PERMISSIONS && !shouldShowPermissions()) {
return;
}
items.add(item);
}
private boolean shouldShowPermissions() {
// Figure out if we should show permissions section
Apk curApk = null;
for (int i = 0; i < versions.size(); i++) {
final Apk apk = versions.get(i);
if (apk.versionCode == app.suggestedVersionCode) {
curApk = apk;
break;
}
}
final boolean curApkCompatible = curApk != null && curApk.compatible;
return versions.size() > 0 && (curApkCompatible || Preferences.get().showIncompatibleVersions());
}
private boolean shouldShowDonate() {
return uriIsSetAndCanBeOpened(app.donateURL) ||
uriIsSetAndCanBeOpened(app.getBitcoinUri()) ||
uriIsSetAndCanBeOpened(app.getLitecoinUri()) ||
uriIsSetAndCanBeOpened(app.getFlattrUri());
}
public void clearProgress() {
setProgress(0, 0, 0);
}
public void setProgress(int bytesDownloaded, int totalBytes, int resIdString) {
if (headerView != null) {
headerView.setProgress(bytesDownloaded, totalBytes, resIdString);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType) {
case VIEWTYPE_HEADER:
View header = inflater.inflate(R.layout.app_details2_header, parent, false);
return new HeaderViewHolder(header);
case VIEWTYPE_SCREENSHOTS:
View screenshots = inflater.inflate(R.layout.app_details2_screenshots, parent, false);
return new ScreenShotsViewHolder(screenshots);
case VIEWTYPE_WHATS_NEW:
View whatsNew = inflater.inflate(R.layout.app_details2_whatsnew, parent, false);
return new WhatsNewViewHolder(whatsNew);
case VIEWTYPE_DONATE:
View donate = inflater.inflate(R.layout.app_details2_donate, parent, false);
return new DonateViewHolder(donate);
case VIEWTYPE_LINKS:
View links = inflater.inflate(R.layout.app_details2_links, parent, false);
return new LinksViewHolder(links);
case VIEWTYPE_PERMISSIONS:
View permissions = inflater.inflate(R.layout.app_details2_links, parent, false);
return new PermissionsViewHolder(permissions);
case VIEWTYPE_VERSIONS:
View versions = inflater.inflate(R.layout.app_details2_links, parent, false);
return new VersionsViewHolder(versions);
case VIEWTYPE_VERSION:
View version = inflater.inflate(R.layout.apklistitem, parent, false);
return new VersionViewHolder(version);
}
return null;
}
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
int viewType = getItemViewType(position);
switch (viewType) {
case VIEWTYPE_HEADER:
HeaderViewHolder header = (HeaderViewHolder) holder;
headerView = header;
header.bindModel();
break;
case VIEWTYPE_SCREENSHOTS:
((ScreenShotsViewHolder) holder).bindModel();
break;
case VIEWTYPE_WHATS_NEW:
((WhatsNewViewHolder) holder).bindModel();
break;
case VIEWTYPE_DONATE:
((DonateViewHolder) holder).bindModel();
break;
case VIEWTYPE_LINKS:
((LinksViewHolder) holder).bindModel();
break;
case VIEWTYPE_PERMISSIONS:
((PermissionsViewHolder) holder).bindModel();
break;
case VIEWTYPE_VERSIONS:
((VersionsViewHolder) holder).bindModel();
break;
case VIEWTYPE_VERSION:
final Apk apk = (Apk) items.get(position);
((VersionViewHolder) holder).bindModel(apk);
break;
}
}
@Override
public void onViewRecycled(RecyclerView.ViewHolder holder) {
if (holder instanceof HeaderViewHolder) {
headerView = null;
}
super.onViewRecycled(holder);
}
@Override
public int getItemCount() {
return items.size();
}
@Override
public int getItemViewType(int position) {
if (items.get(position) instanceof Apk) {
return VIEWTYPE_VERSION;
}
return (Integer) items.get(position);
}
public class HeaderViewHolder extends RecyclerView.ViewHolder {
private static final int MAX_LINES = 5;
final ImageView iconView;
final TextView titleView;
final TextView authorView;
final TextView summaryView;
final TextView descriptionView;
final TextView descriptionMoreView;
final View buttonLayout;
final Button buttonPrimaryView;
final Button buttonSecondaryView;
final View progressLayout;
final ProgressBar progressBar;
final TextView progressLabel;
final TextView progressPercent;
final View progressCancel;
final DisplayImageOptions displayImageOptions;
HeaderViewHolder(View view) {
super(view);
iconView = (ImageView) view.findViewById(R.id.icon);
titleView = (TextView) view.findViewById(R.id.title);
authorView = (TextView) view.findViewById(R.id.author);
summaryView = (TextView) view.findViewById(R.id.summary);
descriptionView = (TextView) view.findViewById(R.id.description);
descriptionMoreView = (TextView) view.findViewById(R.id.description_more);
buttonLayout = view.findViewById(R.id.button_layout);
buttonPrimaryView = (Button) view.findViewById(R.id.primaryButtonView);
buttonSecondaryView = (Button) view.findViewById(R.id.secondaryButtonView);
progressLayout = view.findViewById(R.id.progress_layout);
progressBar = (ProgressBar) view.findViewById(R.id.progress_bar);
progressLabel = (TextView) view.findViewById(R.id.progress_label);
progressPercent = (TextView) view.findViewById(R.id.progress_percent);
progressCancel = view.findViewById(R.id.progress_cancel);
displayImageOptions = new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.imageScaleType(ImageScaleType.NONE)
.showImageOnLoading(R.drawable.ic_repo_app_default)
.showImageForEmptyUri(R.drawable.ic_repo_app_default)
.bitmapConfig(Bitmap.Config.RGB_565)
.build();
descriptionView.setMaxLines(MAX_LINES);
descriptionView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
descriptionMoreView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Make this "header section" the focused child, so that RecyclerView will use
// it as the anchor in the layout process. Otherwise the RV might select another
// view as the anchor, resulting in that the top of this view is instead scrolled
// off the screen. Refer to LinearLayoutManager.updateAnchorFromChildren(...).
recyclerView.requestChildFocus(itemView, itemView);
if (TextViewCompat.getMaxLines(descriptionView) != MAX_LINES) {
descriptionView.setMaxLines(MAX_LINES);
descriptionMoreView.setText(R.string.more);
} else {
descriptionView.setMaxLines(Integer.MAX_VALUE);
descriptionMoreView.setText(R.string.less);
}
}
});
// Set ALL caps (in a way compatible with SDK 10)
AllCapsTransformationMethod allCapsTransformation = new AllCapsTransformationMethod(view.getContext());
buttonPrimaryView.setTransformationMethod(allCapsTransformation);
buttonSecondaryView.setTransformationMethod(allCapsTransformation);
descriptionMoreView.setTransformationMethod(allCapsTransformation);
}
public void setProgress(int bytesDownloaded, int totalBytes, int resIdString) {
if (bytesDownloaded == 0 && totalBytes == 0) {
// Remove progress bar
progressLayout.setVisibility(View.GONE);
buttonLayout.setVisibility(View.VISIBLE);
} else {
progressBar.setMax(totalBytes);
progressBar.setProgress(bytesDownloaded);
progressBar.setIndeterminate(totalBytes == -1);
if (resIdString != 0) {
progressLabel.setText(resIdString);
progressPercent.setText("");
} else if (totalBytes > 0 && bytesDownloaded >= 0) {
float percent = bytesDownloaded * 100 / totalBytes;
progressLabel.setText(Utils.getFriendlySize(bytesDownloaded) + " / " + Utils.getFriendlySize(totalBytes));
NumberFormat format = NumberFormat.getPercentInstance();
format.setMaximumFractionDigits(0);
progressPercent.setText(format.format(percent / 100));
} else if (bytesDownloaded >= 0) {
progressLabel.setText(Utils.getFriendlySize(bytesDownloaded));
progressPercent.setText("");
}
// Make sure it's visible
if (progressLayout.getVisibility() != View.VISIBLE) {
progressLayout.setVisibility(View.VISIBLE);
buttonLayout.setVisibility(View.GONE);
}
}
}
public void bindModel() {
ImageLoader.getInstance().displayImage(app.iconUrlLarge, iconView, displayImageOptions);
titleView.setText(app.name);
if (!TextUtils.isEmpty(app.author)) {
authorView.setText(context.getString(R.string.by_author) + " " + app.author);
authorView.setVisibility(View.VISIBLE);
} else {
authorView.setVisibility(View.GONE);
}
summaryView.setText(app.summary);
final Spanned desc = Html.fromHtml(app.description, null, new Utils.HtmlTagHandler());
descriptionView.setMovementMethod(LinkMovementMethod.getInstance());
descriptionView.setText(trimTrailingNewlines(desc));
if (descriptionView.getText() instanceof Spannable) {
Spannable spannable = (Spannable) descriptionView.getText();
URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
for (URLSpan span : spans) {
int start = spannable.getSpanStart(span);
int end = spannable.getSpanEnd(span);
int flags = spannable.getSpanFlags(span);
spannable.removeSpan(span);
// Create out own safe link span
SafeURLSpan safeUrlSpan = new SafeURLSpan(span.getURL());
spannable.setSpan(safeUrlSpan, start, end, flags);
}
}
descriptionView.post(new Runnable() {
@Override
public void run() {
if (descriptionView.getLineCount() <= HeaderViewHolder.MAX_LINES) {
descriptionMoreView.setVisibility(View.GONE);
} else {
descriptionMoreView.setVisibility(View.VISIBLE);
}
}
});
buttonSecondaryView.setText(R.string.menu_uninstall);
buttonSecondaryView.setVisibility(app.isInstalled() ? View.VISIBLE : View.INVISIBLE);
buttonSecondaryView.setOnClickListener(onUnInstallClickListener);
buttonPrimaryView.setText(R.string.menu_install);
buttonPrimaryView.setVisibility(versions.size() > 0 ? View.VISIBLE : View.GONE);
if (callbacks.isAppDownloading()) {
buttonPrimaryView.setText(R.string.downloading);
buttonPrimaryView.setEnabled(false);
} else if (!app.isInstalled() && app.suggestedVersionCode > 0 && versions.size() > 0) {
// Check count > 0 due to incompatible apps resulting in an empty list.
callbacks.disableAndroidBeam();
// Set Install button and hide second button
buttonPrimaryView.setText(R.string.menu_install);
buttonPrimaryView.setOnClickListener(onInstallClickListener);
buttonPrimaryView.setEnabled(true);
} else if (app.isInstalled()) {
callbacks.enableAndroidBeam();
if (app.canAndWantToUpdate(context)) {
buttonPrimaryView.setText(R.string.menu_upgrade);
buttonPrimaryView.setOnClickListener(onUpgradeClickListener);
} else {
if (context.getPackageManager().getLaunchIntentForPackage(app.packageName) != null) {
buttonPrimaryView.setText(R.string.menu_launch);
buttonPrimaryView.setOnClickListener(onLaunchClickListener);
} else {
buttonPrimaryView.setVisibility(View.GONE);
}
}
buttonPrimaryView.setEnabled(true);
}
if (callbacks.isAppDownloading()) {
buttonLayout.setVisibility(View.GONE);
progressLayout.setVisibility(View.VISIBLE);
} else {
buttonLayout.setVisibility(View.VISIBLE);
progressLayout.setVisibility(View.GONE);
}
progressCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
callbacks.installCancel();
}
});
}
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
this.recyclerView = recyclerView;
}
@Override
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
this.recyclerView = null;
super.onDetachedFromRecyclerView(recyclerView);
}
private class ScreenShotsViewHolder extends RecyclerView.ViewHolder {
final RecyclerView recyclerView;
LinearLayoutManagerSnapHelper snapHelper;
ScreenShotsViewHolder(View view) {
super(view);
recyclerView = (RecyclerView) view.findViewById(R.id.screenshots);
}
public void bindModel() {
LinearLayoutManager lm = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
recyclerView.setLayoutManager(lm);
ScreenShotsRecyclerViewAdapter adapter = new ScreenShotsRecyclerViewAdapter(itemView.getContext(), app);
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(true);
recyclerView.setNestedScrollingEnabled(false);
if (snapHelper != null) {
snapHelper.attachToRecyclerView(null);
}
snapHelper = new LinearLayoutManagerSnapHelper(lm);
snapHelper.setLinearSnapHelperListener(adapter);
snapHelper.attachToRecyclerView(recyclerView);
}
}
private class WhatsNewViewHolder extends RecyclerView.ViewHolder {
final TextView textView;
WhatsNewViewHolder(View view) {
super(view);
textView = (TextView) view.findViewById(R.id.text);
}
public void bindModel() {
textView.setText("WHATS NEW GOES HERE");
}
}
private class DonateViewHolder extends RecyclerView.ViewHolder {
final TextView textView;
final LinearLayout contentView;
DonateViewHolder(View view) {
super(view);
textView = (TextView) view.findViewById(R.id.information);
contentView = (LinearLayout) view.findViewById(R.id.ll_information);
}
public void bindModel() {
contentView.removeAllViews();
// Donate button
if (uriIsSetAndCanBeOpened(app.donateURL)) {
addLinkItemView(contentView, R.string.menu_donate, R.drawable.ic_donate, app.donateURL);
}
// Bitcoin
if (uriIsSetAndCanBeOpened(app.getBitcoinUri())) {
addLinkItemView(contentView, R.string.menu_bitcoin, R.drawable.ic_bitcoin, app.getBitcoinUri());
}
// Litecoin
if (uriIsSetAndCanBeOpened(app.getLitecoinUri())) {
addLinkItemView(contentView, R.string.menu_litecoin, R.drawable.ic_litecoin, app.getLitecoinUri());
}
// Flattr
if (uriIsSetAndCanBeOpened(app.getFlattrUri())) {
addLinkItemView(contentView, R.string.menu_flattr, R.drawable.ic_flattr, app.getFlattrUri());
}
}
}
private abstract class ExpandableLinearLayoutViewHolder extends RecyclerView.ViewHolder {
final TextView headerView;
final LinearLayout contentView;
ExpandableLinearLayoutViewHolder(View view) {
super(view);
headerView = (TextView) view.findViewById(R.id.information);
contentView = (LinearLayout) view.findViewById(R.id.ll_content);
}
}
private class VersionsViewHolder extends ExpandableLinearLayoutViewHolder {
VersionsViewHolder(View view) {
super(view);
}
public void bindModel() {
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setShowVersions(!showVersions);
}
});
headerView.setText(R.string.versions);
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(headerView, R.drawable.ic_access_time_24dp_grey600, 0, showVersions ? R.drawable.ic_expand_less_grey600 : R.drawable.ic_expand_more_grey600, 0);
}
}
private class PermissionsViewHolder extends ExpandableLinearLayoutViewHolder {
PermissionsViewHolder(View view) {
super(view);
}
public void bindModel() {
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean shouldBeVisible = contentView.getVisibility() != View.VISIBLE;
contentView.setVisibility(shouldBeVisible ? View.VISIBLE : View.GONE);
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(headerView, R.drawable.ic_lock_24dp_grey600, 0, shouldBeVisible ? R.drawable.ic_expand_less_grey600 : R.drawable.ic_expand_more_grey600, 0);
}
});
headerView.setText(R.string.permissions);
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(headerView, R.drawable.ic_lock_24dp_grey600, 0, R.drawable.ic_expand_more_grey600, 0);
contentView.removeAllViews();
AppDiff appDiff = new AppDiff(context.getPackageManager(), versions.get(0));
AppSecurityPermissions perms = new AppSecurityPermissions(context, appDiff.pkgInfo);
contentView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL));
}
}
private class LinksViewHolder extends ExpandableLinearLayoutViewHolder {
LinksViewHolder(View view) {
super(view);
}
public void bindModel() {
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean shouldBeVisible = contentView.getVisibility() != View.VISIBLE;
contentView.setVisibility(shouldBeVisible ? View.VISIBLE : View.GONE);
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(headerView, R.drawable.ic_website, 0, shouldBeVisible ? R.drawable.ic_expand_less_grey600 : R.drawable.ic_expand_more_grey600, 0);
}
});
headerView.setText(R.string.links);
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(headerView, R.drawable.ic_website, 0, R.drawable.ic_expand_more_grey600, 0);
contentView.removeAllViews();
// Source button
if (uriIsSetAndCanBeOpened(app.sourceURL)) {
addLinkItemView(contentView, R.string.menu_source, R.drawable.ic_source_code, app.sourceURL);
}
// Issues button
if (uriIsSetAndCanBeOpened(app.trackerURL)) {
addLinkItemView(contentView, R.string.menu_issues, R.drawable.ic_issues, app.trackerURL);
}
// Changelog button
if (uriIsSetAndCanBeOpened(app.changelogURL)) {
addLinkItemView(contentView, R.string.menu_changelog, R.drawable.ic_changelog, app.changelogURL);
}
// Website button
if (uriIsSetAndCanBeOpened(app.webURL)) {
addLinkItemView(contentView, R.string.menu_website, R.drawable.ic_website, app.webURL);
}
// Email button
final String subject = Uri.encode(context.getString(R.string.app_details_subject, app.name));
String emailUrl = app.email == null ? null : ("mailto:" + app.email + "?subject=" + subject);
if (uriIsSetAndCanBeOpened(emailUrl)) {
addLinkItemView(contentView, R.string.menu_email, R.drawable.ic_email, emailUrl);
}
}
}
private class VersionViewHolder extends RecyclerView.ViewHolder {
final TextView version;
final TextView status;
final TextView repository;
final TextView size;
final TextView api;
final TextView incompatibleReasons;
final TextView buildtype;
final TextView added;
final TextView nativecode;
VersionViewHolder(View view) {
super(view);
version = (TextView) view.findViewById(R.id.version);
status = (TextView) view.findViewById(R.id.status);
repository = (TextView) view.findViewById(R.id.repository);
size = (TextView) view.findViewById(R.id.size);
api = (TextView) view.findViewById(R.id.api);
incompatibleReasons = (TextView) view.findViewById(R.id.incompatible_reasons);
buildtype = (TextView) view.findViewById(R.id.buildtype);
added = (TextView) view.findViewById(R.id.added);
nativecode = (TextView) view.findViewById(R.id.nativecode);
int margin = context.getResources().getDimensionPixelSize(R.dimen.layout_horizontal_margin);
int padding = context.getResources().getDimensionPixelSize(R.dimen.details_activity_padding);
ViewCompat.setPaddingRelative(view, margin + padding + ViewCompat.getPaddingStart(view), view.getPaddingTop(), ViewCompat.getPaddingEnd(view), view.getPaddingBottom());
}
public void bindModel(final Apk apk) {
java.text.DateFormat df = DateFormat.getDateFormat(context);
version.setText(context.getString(R.string.version)
+ " " + apk.versionName
+ (apk.versionCode == app.suggestedVersionCode ? "" : ""));
status.setText(getInstalledStatus(apk));
repository.setText(context.getString(R.string.repo_provider,
RepoProvider.Helper.findById(context, apk.repo).getName()));
if (apk.size > 0) {
size.setText(Utils.getFriendlySize(apk.size));
size.setVisibility(View.VISIBLE);
} else {
size.setVisibility(View.GONE);
}
if (!Preferences.get().expertMode()) {
api.setVisibility(View.GONE);
} else if (apk.minSdkVersion > 0 && apk.maxSdkVersion < Apk.SDK_VERSION_MAX_VALUE) {
api.setText(context.getString(R.string.minsdk_up_to_maxsdk,
Utils.getAndroidVersionName(apk.minSdkVersion),
Utils.getAndroidVersionName(apk.maxSdkVersion)));
api.setVisibility(View.VISIBLE);
} else if (apk.minSdkVersion > 0) {
api.setText(context.getString(R.string.minsdk_or_later,
Utils.getAndroidVersionName(apk.minSdkVersion)));
api.setVisibility(View.VISIBLE);
} else if (apk.maxSdkVersion > 0) {
api.setText(context.getString(R.string.up_to_maxsdk,
Utils.getAndroidVersionName(apk.maxSdkVersion)));
api.setVisibility(View.VISIBLE);
}
if (apk.srcname != null) {
buildtype.setText("source");
} else {
buildtype.setText("bin");
}
if (apk.added != null) {
added.setText(context.getString(R.string.added_on,
df.format(apk.added)));
added.setVisibility(View.VISIBLE);
} else {
added.setVisibility(View.GONE);
}
if (Preferences.get().expertMode() && apk.nativecode != null) {
nativecode.setText(TextUtils.join(" ", apk.nativecode));
nativecode.setVisibility(View.VISIBLE);
} else {
nativecode.setVisibility(View.GONE);
}
if (apk.incompatibleReasons != null) {
incompatibleReasons.setText(
context.getResources().getString(
R.string.requires_features,
TextUtils.join(", ", apk.incompatibleReasons)));
incompatibleReasons.setVisibility(View.VISIBLE);
} else {
incompatibleReasons.setVisibility(View.GONE);
}
// Disable it all if it isn't compatible...
final View[] views = {
itemView,
version,
status,
repository,
size,
api,
buildtype,
added,
nativecode,
};
for (final View v : views) {
v.setEnabled(apk.compatible);
}
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
callbacks.installApk(apk);
}
});
}
}
private void addLinkItemView(ViewGroup parent, int resIdText, int resIdDrawable, final String url) {
TextView view = (TextView) LayoutInflater.from(parent.getContext()).inflate(R.layout.app_details2_link_item, parent, false);
view.setText(resIdText);
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(view, resIdDrawable, 0, 0, 0);
parent.addView(view);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onLinkClicked(url);
}
});
}
private String getInstalledStatus(final Apk apk) {
// Definitely not installed.
if (apk.versionCode != app.installedVersionCode) {
return context.getString(R.string.app_not_installed);
}
// Definitely installed this version.
if (apk.sig != null && apk.sig.equals(app.installedSig)) {
return context.getString(R.string.app_installed);
}
// Installed the same version, but from someplace else.
final String installerPkgName;
try {
installerPkgName = context.getPackageManager().getInstallerPackageName(app.packageName);
} catch (IllegalArgumentException e) {
Log.w("AppDetailsAdapter", "Application " + app.packageName + " is not installed anymore");
return context.getString(R.string.app_not_installed);
}
if (TextUtils.isEmpty(installerPkgName)) {
return context.getString(R.string.app_inst_unknown_source);
}
final String installerLabel = InstalledAppProvider
.getApplicationLabel(context, installerPkgName);
return context.getString(R.string.app_inst_known_source, installerLabel);
}
private void onLinkClicked(String url) {
if (!TextUtils.isEmpty(url)) {
callbacks.openUrl(url);
}
}
private final View.OnClickListener onInstallClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
callbacks.installApk();
}
};
private final View.OnClickListener onUnInstallClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
callbacks.uninstallApk();
}
};
private final View.OnClickListener onUpgradeClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
callbacks.upgradeApk();
}
};
private final View.OnClickListener onLaunchClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
callbacks.launchApk();
}
};
private boolean uriIsSetAndCanBeOpened(String s) {
if (TextUtils.isEmpty(s)) {
return false;
}
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(s));
return intent.resolveActivity(context.getPackageManager()) != null;
}
/**
* The HTML formatter adds "\n\n" at the end of every paragraph. This
* is desired between paragraphs, but not at the end of the whole
* string as it adds unwanted spacing at the end of the TextView.
* Remove all trailing newlines.
* Use this function instead of a trim() as that would require
* converting to String and thus losing formatting (e.g. bold).
*/
public static CharSequence trimTrailingNewlines(CharSequence s) {
if (TextUtils.isEmpty(s)) {
return s;
}
int i;
for (i = s.length() - 1; i >= 0; i--) {
if (s.charAt(i) != '\n') {
break;
}
}
if (i == s.length() - 1) {
return s;
}
return s.subSequence(0, i + 1);
}
@SuppressLint("ParcelCreator")
private static final class SafeURLSpan extends URLSpan {
SafeURLSpan(String url) {
super(url);
}
@Override
public void onClick(View widget) {
try {
super.onClick(widget);
} catch (ActivityNotFoundException ex) {
Toast.makeText(widget.getContext(),
widget.getContext().getString(R.string.no_handler_app, getURL()),
Toast.LENGTH_LONG).show();
}
}
}
}

View File

@ -18,7 +18,7 @@ import org.fdroid.fdroid.data.App;
public abstract class AppListAdapter extends CursorAdapter {
private LayoutInflater mInflater;
private LayoutInflater inflater;
private DisplayImageOptions displayImageOptions;
private String upgradeFromTo;
@ -44,7 +44,7 @@ public abstract class AppListAdapter extends CursorAdapter {
}
private void init(Context context) {
mInflater = (LayoutInflater) context.getSystemService(
inflater = (LayoutInflater) context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
displayImageOptions = Utils.getImageLoadingOptions().build();
upgradeFromTo = context.getResources().getString(R.string.upgrade_from_to);
@ -64,7 +64,7 @@ public abstract class AppListAdapter extends CursorAdapter {
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = mInflater.inflate(R.layout.applistitem, parent, false);
View view = inflater.inflate(R.layout.applistitem, parent, false);
ViewHolder holder = new ViewHolder();
holder.name = (TextView) view.findViewById(R.id.name);

View File

@ -0,0 +1,139 @@
package org.fdroid.fdroid.views;
import android.support.annotation.NonNull;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.LinearSnapHelper;
import android.support.v7.widget.OrientationHelper;
import android.support.v7.widget.RecyclerView;
import android.view.View;
public class LinearLayoutManagerSnapHelper extends LinearSnapHelper {
private View lastSavedTarget;
private int lastSavedDistance;
public interface LinearSnapHelperListener {
/**
* Tells the listener that we have selected a view to snap to.
* @param view The selected view (may be null)
* @param position Adapter position of the snapped to view (or NO_POSITION if none)
*/
void onSnappedToView(View view, int position);
}
private final LinearLayoutManager layoutManager;
private final OrientationHelper orientationHelper;
private LinearSnapHelperListener listener;
public LinearLayoutManagerSnapHelper(LinearLayoutManager layoutManager) {
this.layoutManager = layoutManager;
this.orientationHelper = OrientationHelper.createHorizontalHelper(this.layoutManager);
}
public void setLinearSnapHelperListener(LinearSnapHelperListener listener) {
this.listener = listener;
}
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
View snappedView = super.findSnapView(layoutManager);
if (snappedView != null && layoutManager.canScrollHorizontally()) {
if (layoutManager instanceof LinearLayoutManager) {
// The super class implementation will always try to snap the center of a view to the
// center of the screen. This is desired behavior, but will result in that the first
// and last item will never be fully visible (unless in the special case when they all
// fit on the screen)
//
// We handle this by checking if the first (and/or the last) item is visible, and compare
// the distance it would take to "snap" this item to the screen edge to the distance
// needed to snap the "snappedView" to the center of the screen. We always go for the
// smallest distance, e.g. the closest snap position.
//
// To further complicate this, we might have intermediate views in the range 1..idxSnap
// (and correspondingly idxsnap+1..idxLast-1) that will never be "snapped to". We
// interpolate the "snap position" for these views (between center screen and screen edge)
// and then calculate the snap distance for them, again selecting the smallest of them all.
lastSavedTarget = null;
int centerSnapPosition = orientationHelper.getTotalSpace() / 2;
int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
int lastChild = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
int currentSmallestDistance = Integer.MAX_VALUE;
View currentSmallestDistanceView = null;
int snappedViewIndex = ((LinearLayoutManager) layoutManager).getPosition(snappedView);
if (snappedViewIndex != RecyclerView.NO_POSITION) {
int snapPositionFirst = orientationHelper.getDecoratedMeasurement(((LinearLayoutManager) layoutManager).findViewByPosition(firstChild)) / 2;
int snapPositionLast = orientationHelper.getTotalSpace() - orientationHelper.getDecoratedMeasurement(((LinearLayoutManager) layoutManager).findViewByPosition(lastChild)) / 2;
// If first item not on screen, ignore views 0..snappedViewIndex-1
if (firstChild != 0) {
firstChild = snappedViewIndex;
}
// If last item not on screen, ignore views snappedViewIndex+1..N
if (lastChild != this.layoutManager.getItemCount() - 1) {
lastChild = snappedViewIndex;
}
for (int i = firstChild; i <= lastChild; i++) {
View view = ((LinearLayoutManager) layoutManager).findViewByPosition(i);
// Start by interpolating a snap position for (the center of) this view.
//
int snapPosition;
if (i == snappedViewIndex) {
snapPosition = centerSnapPosition;
} else if (i > snappedViewIndex) {
snapPosition = snapPositionLast - (lastChild - i) * (snapPositionLast - centerSnapPosition) / (lastChild - snappedViewIndex);
} else {
snapPosition = snapPositionFirst + (i - firstChild) * (centerSnapPosition - snapPositionFirst) / (snappedViewIndex - firstChild);
}
// Get current position of view (center)
//
int viewPosition = view.getLeft() + view.getWidth() / 2;
// Calculate distance and compare to current best candidate
//
int dist = snapPosition - viewPosition;
if (Math.abs(dist) < Math.abs(currentSmallestDistance) || (Math.abs(dist) == Math.abs(currentSmallestDistance))) {
currentSmallestDistance = dist;
currentSmallestDistanceView = view;
}
}
// Update with best snap candidate
snappedView = currentSmallestDistanceView;
lastSavedTarget = currentSmallestDistanceView;
lastSavedDistance = -currentSmallestDistance;
}
}
}
if (listener != null) {
int snappedPosition = 0;
if (snappedView != null) {
snappedPosition = this.layoutManager.getPosition(snappedView);
}
listener.onSnappedToView(snappedView, snappedPosition);
}
return snappedView;
}
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
if (targetView == lastSavedTarget) {
// No need to recalc, we already did this when finding the snap candidate
//
int[] out = new int[2];
out[0] = lastSavedDistance;
out[1] = 0;
return out;
}
return super.calculateDistanceToFinalSnap(layoutManager, targetView);
}
}

View File

@ -0,0 +1,102 @@
package org.fdroid.fdroid.views;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
public class ScreenShotsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements LinearLayoutManagerSnapHelper.LinearSnapHelperListener {
private final App app;
private final DisplayImageOptions displayImageOptions;
private View selectedView;
private int selectedPosition;
private final int selectedItemElevation;
private final int unselectedItemMargin;
public ScreenShotsRecyclerViewAdapter(Context context, App app) {
super();
this.app = app;
selectedPosition = 0;
selectedItemElevation = context.getResources().getDimensionPixelSize(R.dimen.details_screenshot_selected_elevation);
unselectedItemMargin = context.getResources().getDimensionPixelSize(R.dimen.details_screenshot_margin);
displayImageOptions = new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.imageScaleType(ImageScaleType.NONE)
.showImageOnLoading(R.drawable.ic_repo_app_default)
.showImageForEmptyUri(R.drawable.ic_repo_app_default)
.bitmapConfig(Bitmap.Config.RGB_565)
.build();
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
ScreenShotViewHolder vh = (ScreenShotViewHolder) holder;
setViewSelected(vh.itemView, position == selectedPosition);
if (position == selectedPosition) {
this.selectedView = vh.itemView;
}
ImageLoader.getInstance().displayImage(app.iconUrlLarge, vh.image, displayImageOptions);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.app_details2_screenshot_item, parent, false);
return new ScreenShotViewHolder(view);
}
@Override
public int getItemCount() {
return 7;
}
@Override
public void onSnappedToView(View view, int snappedPosition) {
// Deselect the previous selected view first
setViewSelected(selectedView, false);
// Change the selected view to the newly snapped-to view.
selectedView = view;
selectedPosition = snappedPosition;
setViewSelected(selectedView, true);
}
private void setViewSelected(View view, boolean selected) {
if (view != null) {
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
if (selected) {
lp.setMargins(0, selectedItemElevation, 0, selectedItemElevation);
} else {
lp.setMargins(0, unselectedItemMargin, 0, unselectedItemMargin);
}
ViewCompat.setElevation(view, selected ? selectedItemElevation : selectedItemElevation / 2);
view.setLayoutParams(lp);
}
}
private class ScreenShotViewHolder extends RecyclerView.ViewHolder {
final ImageView image;
ScreenShotViewHolder(View view) {
super(view);
image = (ImageView) view.findViewById(R.id.image);
}
@Override
public String toString() {
return super.toString() + " screenshot";
}
}
}

View File

@ -0,0 +1,213 @@
package org.fdroid.fdroid.views;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.BottomSheetDialogFragment;
import android.support.design.widget.CoordinatorLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import java.util.ArrayList;
import java.util.List;
public class ShareChooserDialog extends BottomSheetDialogFragment {
private static final String ARG_WIDTH = "width";
private static final String ARG_INTENT = "intent";
private static final String ARG_SHOW_NEARBY = "showNearby";
private static final int VIEWTYPE_SWAP = 1;
private static final int VIEWTYPE_INTENT = 0;
private RecyclerView recyclerView;
private ArrayList<ResolveInfo> targets;
private int parentWidth;
private Intent shareIntent;
private boolean showNearby;
public interface ShareChooserDialogListener {
void onNearby();
void onResolvedShareIntent(Intent shareIntent);
}
private ShareChooserDialogListener listener;
private void setListener(ShareChooserDialogListener listener) {
this.listener = listener;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
parentWidth = getArguments().getInt(ARG_WIDTH, 640);
shareIntent = getArguments().getParcelable(ARG_INTENT);
showNearby = getArguments().getBoolean(ARG_SHOW_NEARBY, false);
targets = new ArrayList<>();
List<ResolveInfo> resInfo = getContext().getPackageManager().queryIntentActivities(shareIntent, 0);
if (resInfo != null && resInfo.size() > 0) {
for (ResolveInfo resolveInfo : resInfo) {
String packageName = resolveInfo.activityInfo.packageName;
if (!packageName.equals(BuildConfig.APPLICATION_ID)) { // Remove ourselves
targets.add(resolveInfo);
}
}
}
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Dialog dialog = super.onCreateDialog(savedInstanceState);
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialogInterface) {
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
dialog.getWindow().setLayout(
parentWidth - Utils.dpToPx(0, getContext()), // Set margins here!
ViewGroup.LayoutParams.MATCH_PARENT);
}
});
return dialog;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.share_chooser, container, false);
setupView(v);
return v;
}
private void setupView(View v) {
recyclerView = (RecyclerView) v.findViewById(R.id.recycler_view_apps);
// Figure out how many columns that fit in the given parent width. Give them 100dp.
int appWidth = Utils.dpToPx(80, getContext());
final int nCols = (parentWidth - /* padding */ Utils.dpToPx(8, getContext())) / appWidth;
GridLayoutManager glm = new GridLayoutManager(getContext(), nCols);
// Ensure that if available, the "Nearby Swap" item spans the entire width.
glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (recyclerView.getAdapter() != null) {
if (recyclerView.getAdapter().getItemViewType(position) == VIEWTYPE_SWAP) {
return nCols;
}
return 1;
}
return 0;
}
});
recyclerView.setLayoutManager(glm);
class VH extends RecyclerView.ViewHolder {
public final ImageView icon;
public final TextView label;
VH(View itemView) {
super(itemView);
icon = (ImageView) itemView.findViewById(R.id.ivShare);
label = (TextView) itemView.findViewById(R.id.tvShare);
}
}
recyclerView.setAdapter(new RecyclerView.Adapter<VH>() {
private ArrayList<ResolveInfo> intents;
RecyclerView.Adapter init(List<ResolveInfo> targetedShareIntents) {
intents = new ArrayList<>();
if (showNearby) {
intents.add(null);
}
for (ResolveInfo ri : targetedShareIntents) {
intents.add(ri);
}
return this;
}
@Override
public int getItemViewType(int position) {
if (intents.get(position) == null) {
return VIEWTYPE_SWAP;
}
return VIEWTYPE_INTENT;
}
@Override
public VH onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate((viewType == 1) ? R.layout.share_header_item : R.layout.share_item, parent, false);
return new VH(view);
}
@Override
public void onBindViewHolder(VH holder, int position) {
if (getItemViewType(position) == VIEWTYPE_SWAP) {
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onNearby();
}
dismiss();
}
});
return;
}
final ResolveInfo ri = intents.get(position);
holder.icon.setImageDrawable(ri.loadIcon(getContext().getPackageManager()));
holder.label.setText(ri.loadLabel(getContext().getPackageManager()));
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (listener != null) {
Intent intent = new Intent(shareIntent);
ComponentName name = new ComponentName(ri.activityInfo.applicationInfo.packageName,
ri.activityInfo.name);
intent.setComponent(name);
listener.onResolvedShareIntent(intent);
}
dismiss();
}
});
}
@Override
public int getItemCount() {
return intents.size();
}
}.init(targets));
}
public static void createChooser(CoordinatorLayout rootView, ShareChooserDialog.ShareChooserDialogListener listener, final AppCompatActivity parent, final Intent shareIntent, boolean showNearbyItem) {
ShareChooserDialog d = new ShareChooserDialog();
d.setListener(listener);
Bundle args = new Bundle();
args.putInt(ARG_WIDTH, rootView.getWidth());
args.putParcelable(ARG_INTENT, shareIntent);
args.putBoolean(ARG_SHOW_NEARBY, showNearbyItem);
d.setArguments(args);
d.show(parent.getSupportFragmentManager(), "Share");
}
}

View File

@ -20,6 +20,7 @@ import android.widget.AdapterView;
import android.widget.TextView;
import org.fdroid.fdroid.AppDetails;
import org.fdroid.fdroid.AppDetails2;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
@ -30,6 +31,7 @@ import org.fdroid.fdroid.views.AppListAdapter;
public abstract class AppListFragment extends ListFragment implements
AdapterView.OnItemClickListener,
AdapterView.OnItemLongClickListener,
Preferences.ChangeListener,
LoaderManager.LoaderCallbacks<Cursor> {
@ -109,6 +111,7 @@ public abstract class AppListFragment extends ListFragment implements
// returns the list view is "called between onCreate and
// onActivityCreated" according to the docs.
getListView().setOnItemClickListener(this);
getListView().setOnItemLongClickListener(this);
}
@Override
@ -155,11 +158,21 @@ public abstract class AppListFragment extends ListFragment implements
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
showItemDetails(view, position, false);
}
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
showItemDetails(view, position, true);
return true;
}
private void showItemDetails(View view, int position, boolean useNewDetailsActivity) {
// Cursor is null in the swap list when touching the first item.
Cursor cursor = (Cursor) getListView().getItemAtPosition(position);
if (cursor != null) {
final App app = new App(cursor);
Intent intent = getAppDetailsIntent();
Intent intent = getAppDetailsIntent(useNewDetailsActivity);
intent.putExtra(AppDetails.EXTRA_APPID, app.packageName);
intent.putExtra(AppDetails.EXTRA_FROM, getFromTitle());
if (Build.VERSION.SDK_INT >= 21) {
@ -176,8 +189,8 @@ public abstract class AppListFragment extends ListFragment implements
}
}
private Intent getAppDetailsIntent() {
return new Intent(getActivity(), AppDetails.class);
private Intent getAppDetailsIntent(boolean useNewDetailsActivity) {
return new Intent(getActivity(), useNewDetailsActivity ? AppDetails2.class : AppDetails.class);
}
@Override

View File

@ -71,7 +71,7 @@ public class SelectAppsView extends ListView implements
private static final int LOADER_INSTALLED_APPS = 253341534;
private AppListAdapter adapter;
private String mCurrentFilterString;
private String currentFilterString;
@Override
protected void onFinishInflate() {
@ -154,10 +154,10 @@ public class SelectAppsView extends ListView implements
@Override
public CursorLoader onCreateLoader(int id, Bundle args) {
Uri uri;
if (TextUtils.isEmpty(mCurrentFilterString)) {
if (TextUtils.isEmpty(currentFilterString)) {
uri = InstalledAppProvider.getContentUri();
} else {
uri = InstalledAppProvider.getSearchUri(mCurrentFilterString);
uri = InstalledAppProvider.getSearchUri(currentFilterString);
}
return new CursorLoader(
getActivity(),
@ -192,13 +192,13 @@ public class SelectAppsView extends ListView implements
@Override
public boolean onQueryTextChange(String newText) {
String newFilter = !TextUtils.isEmpty(newText) ? newText : null;
if (mCurrentFilterString == null && newFilter == null) {
if (currentFilterString == null && newFilter == null) {
return true;
}
if (mCurrentFilterString != null && mCurrentFilterString.equals(newFilter)) {
if (currentFilterString != null && currentFilterString.equals(newFilter)) {
return true;
}
mCurrentFilterString = newFilter;
currentFilterString = newFilter;
getActivity().getSupportLoaderManager().restartLoader(LOADER_INSTALLED_APPS, null, this);
return true;
}

View File

@ -89,7 +89,7 @@ public class SwapAppsView extends ListView implements
private Repo repo;
private AppListAdapter adapter;
private String mCurrentFilterString;
private String currentFilterString;
@Override
protected void onFinishInflate() {
@ -190,9 +190,9 @@ public class SwapAppsView extends ListView implements
@Override
public CursorLoader onCreateLoader(int id, Bundle args) {
Uri uri = TextUtils.isEmpty(mCurrentFilterString)
Uri uri = TextUtils.isEmpty(currentFilterString)
? AppProvider.getRepoUri(repo)
: AppProvider.getSearchUri(repo, mCurrentFilterString);
: AppProvider.getSearchUri(repo, currentFilterString);
return new CursorLoader(getActivity(), uri, AppMetadataTable.Cols.ALL, null, null, AppMetadataTable.Cols.NAME);
}
@ -210,13 +210,13 @@ public class SwapAppsView extends ListView implements
@Override
public boolean onQueryTextChange(String newText) {
String newFilter = !TextUtils.isEmpty(newText) ? newText : null;
if (mCurrentFilterString == null && newFilter == null) {
if (currentFilterString == null && newFilter == null) {
return true;
}
if (mCurrentFilterString != null && mCurrentFilterString.equals(newFilter)) {
if (currentFilterString != null && currentFilterString.equals(newFilter)) {
return true;
}
mCurrentFilterString = newFilter;
currentFilterString = newFilter;
getActivity().getSupportLoaderManager().restartLoader(LOADER_SWAPABLE_APPS, null, this);
return true;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@color/fdroid_blue_dark" />
</shape>
</item>
<item android:state_checked="true">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@color/fdroid_blue_dark" />
</shape>
</item>
<item android:state_enabled="false">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@color/fdroid_blue_dark" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@color/fdroid_blue" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@color/fdroid_blue" />
</shape>
</item>
<item android:state_checked="true">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@color/fdroid_blue" />
</shape>
</item>
<item android:state_enabled="false">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@color/fdroid_blue" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@android:color/white" />
<stroke android:color="@color/fdroid_blue" android:width="2dp" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@color/details_panel_light" />
</shape>

View File

@ -0,0 +1,18 @@
<vector android:height="48dp" android:viewportHeight="100.0"
android:viewportWidth="100.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#0066CC"
android:pathData="M82.21,71.93C75.18,82.46 63.17,89.42 49.55,89.42C45.85,89.42 42.29,88.87 38.9,87.92C37.72,90.05 36.2,91.96 34.44,93.61C39.18,95.26 44.25,96.2 49.55,96.2C66.77,96.2 81.8,86.71 89.68,72.73C89.17,72.77 88.65,72.81 88.12,72.81C86.07,72.81 84.08,72.5 82.21,71.93"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#0066CC"
android:pathData="M10.34,50.39C10.34,28.87 27.93,11.37 49.55,11.37C64.69,11.37 77.83,19.97 84.36,32.51C85.58,32.28 86.84,32.15 88.12,32.15C89.42,32.15 90.68,32.28 91.91,32.51C84.89,16.12 68.55,4.59 49.55,4.59C24.18,4.59 3.54,25.14 3.54,50.39C3.54,53.15 3.82,55.84 4.3,58.46C6.2,57 8.33,55.82 10.64,54.99C10.46,53.48 10.34,51.95 10.34,50.39"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#0066CC"
android:pathData="M49.55,74.61C58.46,74.61 66.24,69.81 70.48,62.68C68.72,59.68 67.7,56.2 67.7,52.47C67.7,47.96 69.2,43.8 71.7,40.43C67.87,32.03 59.4,26.17 49.55,26.17C36.13,26.17 25.22,37.04 25.22,50.39C25.22,51.87 25.37,53.3 25.62,54.7C33.91,57.34 40.15,64.47 41.49,73.22C44.02,74.11 46.72,74.61 49.55,74.61L49.55,74.61ZM35.05,50.39C35.05,42.43 41.55,35.96 49.55,35.96C57.54,35.96 64.05,42.43 64.05,50.39C64.05,58.35 57.54,64.82 49.55,64.82C41.55,64.82 35.05,58.35 35.05,50.39L35.05,50.39Z"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#0066CC"
android:pathData="M18.53,65.2C12.13,65.2 6.93,70.37 6.93,76.74C6.93,83.12 12.13,88.29 18.53,88.29C24.94,88.29 30.13,83.12 30.13,76.74C30.13,70.37 24.94,65.2 18.53,65.2"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#0066CC"
android:pathData="M88.12,43.19C82.97,43.19 78.79,47.35 78.79,52.48C78.79,57.61 82.97,61.77 88.12,61.77C93.27,61.77 97.45,57.61 97.45,52.48C97.45,47.35 93.27,43.19 88.12,43.19"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="org.fdroid.fdroid.AppDetails2"
android:background="#fcfcfc"
android:id="@+id/rootCoordinator"
>
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/app_bar_height"
android:fitsSystemWindows="true"
android:theme="@style/AppThemeLight.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:id="@+id/feature_graphic"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:fitsSystemWindows="true"
android:src="@drawable/feature_placeholder"
app:layout_collapseMode="parallax" />
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppThemeLight.PopupOverlay" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rvDetails"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:behavior_overlapTop="40dp"
app:layoutManager="LinearLayoutManager"
tools:context="org.fdroid.fdroid.AppDetails2"
tools:showIn="@layout/app_details2"
>
</android.support.v7.widget.RecyclerView>
</android.support.design.widget.CoordinatorLayout>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ll_information"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/details_activity_padding"
android:clickable="true"
android:orientation="vertical"
android:background="@drawable/details_panel_light_background"
android:padding="@dimen/details_activity_padding"
tools:ignore="UnusedAttribute">
<TextView
android:id="@+id/information"
style="@style/AppDetailsSubheaderText"
android:text="@string/menu_donate"
/>
</LinearLayout>

View File

@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/details_activity_padding"
app:cardBackgroundColor="#ffffff"
app:cardCornerRadius="3dp"
app:cardElevation="3dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<ImageView
android:id="@+id/icon"
android:layout_width="72dp"
android:layout_height="72dp"
android:paddingRight="8dp"
android:paddingBottom="8dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_repo_app_default" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_toEndOf="@id/icon"
android:layout_toRightOf="@id/icon"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
tools:text="App Title" />
<TextView
android:id="@+id/author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignEnd="@id/title"
android:layout_alignLeft="@id/title"
android:layout_alignRight="@id/title"
android:layout_alignStart="@id/title"
android:layout_below="@id/title"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
tools:text="Author" />
<RelativeLayout
android:id="@+id/progress_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/icon"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true">
<ImageView
android:id="@+id/progress_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:src="@android:drawable/ic_menu_close_clear_cancel"
/>
<TextView
android:id="@+id/progress_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:text="@string/downloading"
/>
<TextView
android:id="@+id/progress_percent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/progress_cancel"
android:layout_toStartOf="@id/progress_cancel"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:text=""
/>
<ProgressBar
android:id="@+id/progress_bar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/progress_cancel"
android:layout_toStartOf="@id/progress_cancel"
android:layout_below="@id/progress_label"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
/>
</RelativeLayout>
<LinearLayout
android:id="@+id/button_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/icon"
android:layout_toEndOf="@id/icon"
android:layout_toRightOf="@id/icon"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:visibility="gone"
>
<Button
android:id="@+id/secondaryButtonView"
style="@style/DetailsSecondaryButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:maxLines="1"
android:ellipsize="marquee"
android:text="THIS IS BUTTON 1" />
<Button
android:id="@+id/primaryButtonView"
style="@style/DetailsPrimaryButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:maxLines="1"
android:ellipsize="marquee"
android:text="THIS IS 2" />
</LinearLayout>
</RelativeLayout>
<TextView
android:id="@+id/summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
tools:text="This app is awezome"
android:scrollbars="none"
android:textStyle="bold"
/>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
tools:text="This is the app description of this awezome app. It can be several lines long, but will be truncated at just a few if it is. A 'read more' button will appear so that you can expand the view and view the full text, if you wish. Yes, it will be blue and beautiful."
android:scrollbars="none"
/>
<TextView
android:id="@+id/description_more"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
style="@style/DetailsMoreButtonStyle"
android:gravity="right|end"
android:text="@string/more"
tools:text="more" />
</LinearLayout>
</android.support.v7.widget.CardView>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
style="@style/AppDetailsLink"
android:drawableLeft="@drawable/ic_website"
android:drawableStart="@drawable/ic_website"
android:layout_marginLeft="@dimen/layout_horizontal_margin"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:text="@string/menu_website"
xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ll_information"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/details_activity_padding"
android:clickable="true"
android:orientation="vertical"
tools:ignore="UnusedAttribute">
<TextView
android:id="@+id/information"
style="@style/AppDetailsSubheaderText"
android:text="@string/links"
android:drawableRight="@drawable/ic_expand_more_grey600"
android:drawableEnd="@drawable/ic_expand_more_grey600"
android:drawableLeft="@drawable/ic_website"
android:drawableStart="@drawable/ic_website" />
<LinearLayout
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="@dimen/layout_horizontal_margin"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:visibility="gone">
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardElevation="3dp"
app:cardBackgroundColor="#ffffff"
android:padding="10dp"
>
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:minWidth="@dimen/details_screenshot_width"
android:scaleType="fitCenter"
/>
</android.support.v7.widget.CardView>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/screenshots"
android:layout_width="match_parent"
android:layout_height="@dimen/details_screenshot_height">
</android.support.v7.widget.RecyclerView>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/details_activity_padding"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
/>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/share_root"
android:gravity="center_horizontal|bottom"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:background="@android:color/transparent"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="@string/menu_share"
android:id="@+id/textView2"
android:layout_marginBottom="10dp"
/>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view_apps"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff" />
</LinearLayout>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingBottom="20dp"
>
<ImageView
android:id="@+id/icon"
android:layout_width="@android:dimen/app_icon_size"
android:layout_height="@android:dimen/app_icon_size"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:scaleType="centerCrop"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_nearby_share"
android:layout_marginRight="10dp"
android:layout_marginEnd="10dp"
/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_toEndOf="@id/icon"
android:layout_toRightOf="@id/icon"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:text="@string/swap_nearby"
android:textColor="@android:color/black"
android:textStyle="bold"
android:gravity="start"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/title"
android:layout_alignStart="@id/title"
android:layout_alignEnd="@id/title"
android:layout_alignRight="@id/title"
android:layout_below="@id/title"
android:layout_alignParentBottom="true"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:text="@string/swap_intro"
android:id="@+id/tvShare"
android:textColor="@android:color/darker_gray"
android:gravity="start"
/>
</RelativeLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:minWidth="80dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
>
<ImageView
android:layout_width="@android:dimen/app_icon_size"
android:layout_height="@android:dimen/app_icon_size"
android:id="@+id/ivShare"
android:scaleType="centerCrop"
android:layout_gravity="center_horizontal" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:text="Small Text"
android:id="@+id/tvShare"
android:textColor="@android:color/black"
android:layout_marginTop="8dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:textSize="12sp"
android:fontFamily="sans-serif-condensed"
android:gravity="top|center_horizontal"
android:minLines="2"
android:maxLines="2"
android:ellipsize="marquee"
/>
</LinearLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
<item
android:id="@+id/action_share"
android:icon="@drawable/ic_share_white"
android:title="@string/menu_share"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_ignore_all"
android:icon="@drawable/ic_do_not_disturb_white"
android:title="@string/menu_ignore_all"
android:checkable="true"
app:showAsAction="never"/>
<item
android:id="@+id/action_ignore_this"
android:icon="@drawable/ic_do_not_disturb_white"
android:title="@string/menu_ignore_this"
android:checkable="true"
app:showAsAction="never"/>
</menu>

View File

@ -18,4 +18,11 @@
<item name="android:colorButtonNormal">#04b9e6</item>
</style>
<style name="AppThemeLight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>

View File

@ -24,5 +24,5 @@
<color name="shadow">#cc222222</color>
<color name="perms_costs_money">#fff4511e</color>
<color name="details_panel_light">#eff4f9</color>
</resources>

View File

@ -3,5 +3,14 @@
<dimen name="layout_horizontal_margin">16dp</dimen>
<dimen name="material_listitem_height">48dp</dimen>
<dimen name="app_bar_height">180dp</dimen>
<dimen name="details_activity_padding">8dp</dimen>
<dimen name="details_screenshot_height">200dp</dimen>
<dimen name="details_screenshot_width">200dp</dimen>
<!-- "Not selected" items are inset by this value, while the selected one is not -->
<dimen name="details_screenshot_margin">8dp</dimen>
<!-- The selected item stands out from the background by this elevation -->
<dimen name="details_screenshot_selected_elevation">3dp</dimen>
</resources>

View File

@ -70,6 +70,7 @@
<string name="repo_add_title">Add new repository</string>
<string name="repo_add_add">Add</string>
<string name="links">Links</string>
<string name="versions">Versions</string>
<string name="more">More</string>
<string name="less">Less</string>

View File

@ -246,4 +246,14 @@
<style name="AppDetailsSubheaderText" parent="AppDetailsSubheaderTextBase" />
<style name="AppThemeTransparent" parent="@android:style/Theme.NoDisplay" />
<style name="AppThemeLight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="colorControlNormal">@android:color/white</item>
</style>
<style name="AppThemeLight.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppThemeLight.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="DetailsPrimaryButtonStyle">
<item name="android:padding">5dp</item>
<item name="android:textSize">12sp</item>
<item name="android:textStyle">normal</item>
<item name="android:textColor">#ffffff</item>
<item name="android:background">@drawable/button_primary_background_selector</item>
</style>
<style name="DetailsSecondaryButtonStyle">
<item name="android:padding">5dp</item>
<item name="android:textSize">12sp</item>
<item name="android:textStyle">normal</item>
<item name="android:textColor">@color/fdroid_blue</item>
<item name="android:background">@drawable/button_secondary_background_selector</item>
</style>
<style name="DetailsMoreButtonStyle">
<item name="android:padding">5dp</item>
<item name="android:textSize">15sp</item>
<item name="android:textStyle">normal</item>
<item name="android:textColor">@color/fdroid_blue</item>
</style>
</resources>

View File

@ -3,6 +3,7 @@ package org.fdroid.fdroid;
import android.content.Context;
import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
@ -48,6 +49,15 @@ public class UtilsTest {
String fingerprintLongByOneFingerprint = "59050C8155DCA377F23D5A15B77D37134000CDBD8B42FBFBE0E3F38096E68CECE";
String fingerprintLongByOnePubkey = "308203c5308202ada00302010202047b7cf549300d06092a864886f70d01010b0500308192310b30090603550406130255533111300f060355040813084e657720596f726b3111300f060355040713084e657720596f726b311d301b060355040a131454686520477561726469616e2050726f6a656374311f301d060355040b1316477561726469616e20462d44726f6964204275696c64311d301b06035504031314677561726469616e70726f6a6563742e696e666f301e170d3132313032393130323530305a170d3430303331363130323530305a308192310b30090603550406130255533111300f060355040813084e657720596f726b3111300f060355040713084e657720596f726b311d301b060355040a131454686520477561726469616e2050726f6a656374311f301d060355040b1316477561726469616e20462d44726f6964204275696c64311d301b06035504031314677561726469616e70726f6a6563742e696e666f30820122300d06092a864886f70d01010105000382010f003082010a0282010100b7f1f635fa3fce1a8042aaa960c2dc557e4ad2c082e5787488cba587fd26207cf59507919fc4dcebda5c8c0959d14146d0445593aa6c29dc639570b71712451fd5c231b0c9f5f0bec380503a1c2a3bc00048bc5db682915afa54d1ecf67b45e1e05c0934b3037a33d3a565899131f27a72c03a5de93df17a2376cc3107f03ee9d124c474dfab30d4053e8f39f292e2dcb6cc131bce12a0c5fc307985195d256bf1d7a2703d67c14bf18ed6b772bb847370b20335810e337c064fef7e2795a524c664a853cd46accb8494f865164dabfb698fa8318236432758bc40d52db00d5ce07fe2210dc06cd95298b4f09e6c9b7b7af61c1d62ea43ea36a2331e7b2d4e250203010001a321301f301d0603551d0e0416041404d763e981cf3a295b94a790d8536a783097232b300d06092a864886f70d01010b05000382010100654e6484ff032c54fed1d96d3c8e731302be9dbd7bb4fe635f2dac05b69f3ecbb5acb7c9fe405e2a066567a8f5c2beb8b199b5a4d5bb1b435cf02df026d4fb4edd9d8849078f085b00950083052d57467d65c6eebd98f037cff9b148d621cf8819c4f7dc1459bf8fc5c7d76f901495a7caf35d1e5c106e1d50610c4920c3c1b50adcfbd4ad83ce7353cdea7d856bba0419c224f89a2f3ebc203d20eb6247711ad2b55fd4737936dc42ced7a047cbbd24012079204a2883b6d55d5d5b66d9fd82fb51fca9a5db5fad9af8564cb380ff30ae8263dbbf01b46e01313f53279673daa3f893380285646b244359203e7eecde94ae141b7dfa8e6499bb8e7e0b25ab85";
@Test
public void trailingNewLines() {
CharSequence threeParagraphs = AppDetailsRecyclerViewAdapter.trimTrailingNewlines("Paragraph One\n\nParagraph Two\n\nParagraph Three\n\n");
assertEquals("Paragraph One\n\nParagraph Two\n\nParagraph Three", threeParagraphs);
CharSequence leadingAndExtraTrailing = AppDetailsRecyclerViewAdapter.trimTrailingNewlines("\n\n\nA\n\n\n");
assertEquals("\n\n\nA", leadingAndExtraTrailing);
}
@Test
public void commaSeparatedStrings() {
assertNull(Utils.parseCommaSeparatedString(null));

View File

@ -0,0 +1,120 @@
package org.fdroid.fdroid.views;
import android.app.Application;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.FDroidProvider;
import org.fdroid.fdroid.data.FDroidProviderTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
// TODO: Use sdk=24 when Robolectric supports this
@Config(constants = BuildConfig.class, application = Application.class, sdk = 23)
@RunWith(RobolectricGradleTestRunner.class)
public class AppDetailsAdapterTest extends FDroidProviderTest {
@Before
public void setup() {
ImageLoader.getInstance().init(ImageLoaderConfiguration.createDefault(context));
}
@After
public void teardown() {
ImageLoader.getInstance().destroy();
FDroidProvider.clearDbHelperSingleton();
}
@Test
public void appWithNoVersions() {
App app = new App();
app.name = "Test App";
app.description = "Test App <b>Description</b>";
AppDetailsRecyclerViewAdapter adapter = new AppDetailsRecyclerViewAdapter(context, app, dummyCallbacks);
populateViewHolders(adapter);
assertEquals(5, adapter.getItemCount());
}
/**
* Ensures that every single item in the adapter gets its view holder created and bound.
* Doesn't care about what type of holder it should be, the adapter is able to figure all that
* out for us .
*/
private void populateViewHolders(RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
ViewGroup parent = (ViewGroup) LayoutInflater.from(context).inflate(R.layout.app_details2_links, null);
for (int i = 0; i < adapter.getItemCount(); i++) {
RecyclerView.ViewHolder viewHolder = adapter.createViewHolder(parent, adapter.getItemViewType(i));
adapter.bindViewHolder(viewHolder, i);
}
}
private final AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks dummyCallbacks = new AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks() {
@Override
public boolean isAppDownloading() {
return false;
}
@Override
public void enableAndroidBeam() {
}
@Override
public void disableAndroidBeam() {
}
@Override
public void openUrl(String url) {
}
@Override
public void installApk() {
}
@Override
public void installApk(Apk apk) {
}
@Override
public void upgradeApk() {
}
@Override
public void uninstallApk() {
}
@Override
public void installCancel() {
}
@Override
public void launchApk() {
}
};
}

View File

@ -20,7 +20,9 @@
</module>
<module name="LocalFinalVariableName" />
<module name="LocalVariableName" />
<module name="MemberName" />
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
</module>
<module name="MethodName" />
<module name="PackageName" />
<module name="ParameterName" />