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
	 Peter Serwylo
						Peter Serwylo