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:
commit
f9f0a0f91c
@ -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',
|
||||
|
@ -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"
|
||||
|
@ -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();
|
||||
|
575
app/src/main/java/org/fdroid/fdroid/AppDetails2.java
Normal file
575
app/src/main/java/org/fdroid/fdroid/AppDetails2.java
Normal 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();
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
BIN
app/src/main/res/drawable-hdpi/ic_access_time_24dp_grey600.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_access_time_24dp_grey600.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 657 B |
BIN
app/src/main/res/drawable-mdpi/feature_placeholder.png
Normal file
BIN
app/src/main/res/drawable-mdpi/feature_placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 346 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_access_time_24dp_grey600.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_access_time_24dp_grey600.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 427 B |
BIN
app/src/main/res/drawable-xhdpi/ic_access_time_24dp_grey600.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_access_time_24dp_grey600.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 880 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_access_time_24dp_grey600.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_access_time_24dp_grey600.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
@ -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>
|
@ -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>
|
@ -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>
|
18
app/src/main/res/drawable/ic_nearby_share.xml
Normal file
18
app/src/main/res/drawable/ic_nearby_share.xml
Normal 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>
|
62
app/src/main/res/layout/app_details2.xml
Normal file
62
app/src/main/res/layout/app_details2.xml
Normal 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>
|
19
app/src/main/res/layout/app_details2_donate.xml
Normal file
19
app/src/main/res/layout/app_details2_donate.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns: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>
|
177
app/src/main/res/layout/app_details2_header.xml
Executable file
177
app/src/main/res/layout/app_details2_header.xml
Executable 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>
|
9
app/src/main/res/layout/app_details2_link_item.xml
Normal file
9
app/src/main/res/layout/app_details2_link_item.xml
Normal 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" />
|
30
app/src/main/res/layout/app_details2_links.xml
Normal file
30
app/src/main/res/layout/app_details2_links.xml
Normal 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>
|
18
app/src/main/res/layout/app_details2_screenshot_item.xml
Normal file
18
app/src/main/res/layout/app_details2_screenshot_item.xml
Normal 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>
|
7
app/src/main/res/layout/app_details2_screenshots.xml
Normal file
7
app/src/main/res/layout/app_details2_screenshots.xml
Normal 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>
|
8
app/src/main/res/layout/app_details2_whatsnew.xml
Executable file
8
app/src/main/res/layout/app_details2_whatsnew.xml
Executable 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"
|
||||
/>
|
24
app/src/main/res/layout/share_chooser.xml
Normal file
24
app/src/main/res/layout/share_chooser.xml
Normal 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>
|
57
app/src/main/res/layout/share_header_item.xml
Normal file
57
app/src/main/res/layout/share_header_item.xml
Normal 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>
|
38
app/src/main/res/layout/share_item.xml
Normal file
38
app/src/main/res/layout/share_item.xml
Normal 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>
|
22
app/src/main/res/menu/details2.xml
Normal file
22
app/src/main/res/menu/details2.xml
Normal 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>
|
@ -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>
|
||||
|
@ -24,5 +24,5 @@
|
||||
<color name="shadow">#cc222222</color>
|
||||
|
||||
<color name="perms_costs_money">#fff4511e</color>
|
||||
|
||||
<color name="details_panel_light">#eff4f9</color>
|
||||
</resources>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
26
app/src/main/res/values/styles_detail.xml
Normal file
26
app/src/main/res/values/styles_detail.xml
Normal 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>
|
@ -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));
|
||||
|
@ -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() {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -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" />
|
||||
|
Loading…
x
Reference in New Issue
Block a user