Merge branch 'swap/refinement-towards-stable'
This commit is contained in:
		
						commit
						0db2499666
					
				| @ -12,6 +12,8 @@ | |||||||
| 
 | 
 | ||||||
| * Handle amazon.com and play.google.com app links | * Handle amazon.com and play.google.com app links | ||||||
| 
 | 
 | ||||||
|  | * Misc fixes to the "swap" workflow (especially on Android 2.3 devices) | ||||||
|  | 
 | ||||||
| ### 0.83 (2015-03-26) | ### 0.83 (2015-03-26) | ||||||
| 
 | 
 | ||||||
| * Fix possible crashes when installing or uninstalling apps | * Fix possible crashes when installing or uninstalling apps | ||||||
|  | |||||||
| @ -329,6 +329,14 @@ | |||||||
|             </intent-filter> |             </intent-filter> | ||||||
| 
 | 
 | ||||||
|         </activity> |         </activity> | ||||||
|  |         <activity | ||||||
|  |             android:name=".views.swap.SwapAppListActivity$SwapAppDetails" | ||||||
|  |             android:label="@string/app_details" | ||||||
|  |             android:parentActivityName=".views.swap.SwapAppListActivity" > | ||||||
|  |             <meta-data | ||||||
|  |                 android:name="android.support.PARENT_ACTIVITY" | ||||||
|  |                 android:value=".views.swap.SwapAppListActivity" /> | ||||||
|  |         </activity> | ||||||
|         <activity |         <activity | ||||||
|             android:label="@string/menu_preferences" |             android:label="@string/menu_preferences" | ||||||
|             android:name=".PreferencesActivity" |             android:name=".PreferencesActivity" | ||||||
|  | |||||||
| @ -39,6 +39,8 @@ if ( !hasProperty( 'sourceDeps' ) ) { | |||||||
| 
 | 
 | ||||||
|         // Upstream doesn't have a binary on mavenCentral. |         // Upstream doesn't have a binary on mavenCentral. | ||||||
|         compile(name: 'zipsigner') |         compile(name: 'zipsigner') | ||||||
|  | 
 | ||||||
|  |         androidTestCompile 'commons-io:commons-io:2.2' | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } else { | } else { | ||||||
| @ -76,6 +78,8 @@ if ( !hasProperty( 'sourceDeps' ) ) { | |||||||
|         compile 'com.android.support:support-v4:20.0.+', |         compile 'com.android.support:support-v4:20.0.+', | ||||||
|                 'com.android.support:appcompat-v7:20.0.+', |                 'com.android.support:appcompat-v7:20.0.+', | ||||||
|                 'com.android.support:support-annotations:20.0.+' |                 'com.android.support:support-annotations:20.0.+' | ||||||
|  | 
 | ||||||
|  |         androidTestCompile 'commons-io:commons-io:2.2' | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| @ -135,7 +139,16 @@ android { | |||||||
|             assets.srcDirs = ['assets'] |             assets.srcDirs = ['assets'] | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         instrumentTest.setRoot('test') |         androidTest.setRoot('test') | ||||||
|  |         androidTest { | ||||||
|  |             manifest.srcFile 'test/AndroidManifest.xml' | ||||||
|  |             java.srcDirs = ['test/src'] | ||||||
|  |             resources.srcDirs = ['test/src'] | ||||||
|  |             aidl.srcDirs = ['test/src'] | ||||||
|  |             renderscript.srcDirs = ['test/src'] | ||||||
|  |             res.srcDirs = ['test/res'] | ||||||
|  |             assets.srcDirs = ['test/assets'] | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     buildTypes { |     buildTypes { | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ | |||||||
|     android:paddingTop="2dip" > |     android:paddingTop="2dip" > | ||||||
| 
 | 
 | ||||||
|     <ImageView |     <ImageView | ||||||
|  |         android:id="@android:id/icon" | ||||||
|         android:layout_width="48dip" |         android:layout_width="48dip" | ||||||
|         android:layout_height="48dip" |         android:layout_height="48dip" | ||||||
|         android:layout_marginLeft="?attr/listPreferredItemPaddingLeft" |         android:layout_marginLeft="?attr/listPreferredItemPaddingLeft" | ||||||
|  | |||||||
| @ -20,7 +20,13 @@ | |||||||
|     android:paddingBottom="2dip" |     android:paddingBottom="2dip" | ||||||
|     android:paddingTop="2dip" > |     android:paddingTop="2dip" > | ||||||
| 
 | 
 | ||||||
|  |     <CheckBox | ||||||
|  |         android:id="@+id/checkbox" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" /> | ||||||
|  | 
 | ||||||
|     <ImageView |     <ImageView | ||||||
|  |         android:id="@android:id/icon" | ||||||
|         android:layout_width="48dip" |         android:layout_width="48dip" | ||||||
|         android:layout_height="48dip" |         android:layout_height="48dip" | ||||||
|         android:layout_marginLeft="?attr/listPreferredItemPaddingLeft" |         android:layout_marginLeft="?attr/listPreferredItemPaddingLeft" | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								F-Droid/res/layout/swap_activity.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								F-Droid/res/layout/swap_activity.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | <?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="match_parent"> | ||||||
|  | 
 | ||||||
|  |     <FrameLayout | ||||||
|  |             android:id="@+id/fragment_container" | ||||||
|  |             android:layout_width="fill_parent" | ||||||
|  |             android:layout_height="fill_parent"> | ||||||
|  | 
 | ||||||
|  |     </FrameLayout> | ||||||
|  | 
 | ||||||
|  | </RelativeLayout> | ||||||
| @ -8,14 +8,14 @@ | |||||||
| 
 | 
 | ||||||
|     <TextView |     <TextView | ||||||
|             android:id="@+id/text_description" |             android:id="@+id/text_description" | ||||||
|             android:text="Your mobile device becomes an app store with Swap!" |             android:text="@string/swap_introduction" | ||||||
|             style="@style/SwapTheme.StartSwap.MainText" |             style="@style/SwapTheme.StartSwap.MainText" | ||||||
|             android:layout_width="wrap_content" |             android:layout_width="wrap_content" | ||||||
|             android:layout_height="wrap_content" /> |             android:layout_height="wrap_content" /> | ||||||
| 
 | 
 | ||||||
|     <Button |     <Button | ||||||
|             android:id="@+id/button_start_swap" |             android:id="@+id/button_start_swap" | ||||||
|             android:text="START A SWAP" |             android:text="@string/swap_start" | ||||||
|             style="@style/SwapTheme.StartSwap.StartButton" |             style="@style/SwapTheme.StartSwap.StartButton" | ||||||
|             android:layout_width="match_parent" |             android:layout_width="match_parent" | ||||||
|             android:layout_below="@+id/text_description" |             android:layout_below="@+id/text_description" | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								F-Droid/res/menu/swap_next_search.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								F-Droid/res/menu/swap_next_search.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | <menu xmlns:android="http://schemas.android.com/apk/res/android" > | ||||||
|  | 
 | ||||||
|  |     <item | ||||||
|  |         android:id="@+id/action_next" | ||||||
|  |         android:title="Next" | ||||||
|  |         android:titleCondensed="Next"/> | ||||||
|  | 
 | ||||||
|  |     <item | ||||||
|  |         android:id="@+id/action_search" | ||||||
|  |         android:icon="@android:drawable/ic_menu_search" | ||||||
|  |         android:title="Search" | ||||||
|  |         android:titleCondensed="Search"/> | ||||||
|  | 
 | ||||||
|  | </menu> | ||||||
| @ -307,4 +307,6 @@ | |||||||
|     <string name="open_qr_code_scanner">Open QR Code Scanner</string> |     <string name="open_qr_code_scanner">Open QR Code Scanner</string> | ||||||
|     <string name="swap_welcome">Welcome to F-Droid!</string> |     <string name="swap_welcome">Welcome to F-Droid!</string> | ||||||
|     <string name="swap_confirm_connect">Do you want to get apps from %1$s now?</string> |     <string name="swap_confirm_connect">Do you want to get apps from %1$s now?</string> | ||||||
|  |     <string name="swap_introduction">Your mobile device becomes an app store with Swap!</string> | ||||||
|  |     <string name="swap_start">START A SWAP</string> | ||||||
| </resources> | </resources> | ||||||
|  | |||||||
| @ -12,14 +12,14 @@ | |||||||
|         <!-- backward-compatibility theme options go here --> |         <!-- backward-compatibility theme options go here --> | ||||||
|     </style> |     </style> | ||||||
| 
 | 
 | ||||||
| 	<color name="black">#FF000000</color> |     <color name="black">#FF000000</color> | ||||||
| 	<color name="white">#FFFFFFFF</color> |     <color name="white">#FFFFFFFF</color> | ||||||
| 	<color name="red">#FFFF0000</color> |     <color name="red">#FFFF0000</color> | ||||||
| 
 | 
 | ||||||
| 	<style name="AboutDialogLight" parent="@android:style/Theme.Dialog"> |     <style name="AboutDialogLight" parent="@android:style/Theme.Dialog"> | ||||||
| 		<item name="@android:windowBackground">@color/black</item> |         <item name="@android:windowBackground">@color/black</item> | ||||||
| 		<item name="@android:textColor">@color/white</item> |         <item name="@android:textColor">@color/white</item> | ||||||
| 	</style> |     </style> | ||||||
| 
 | 
 | ||||||
|     <style name="AppThemeDark" parent="AppBaseThemeDark"> |     <style name="AppThemeDark" parent="AppBaseThemeDark"> | ||||||
|         <!-- customizations that are not API-level specific go here. --> |         <!-- customizations that are not API-level specific go here. --> | ||||||
| @ -65,6 +65,7 @@ | |||||||
| 
 | 
 | ||||||
|     <style name="SwapTheme.AppList.SwapSuccess"> |     <style name="SwapTheme.AppList.SwapSuccess"> | ||||||
|         <item name="android:textAlignment">center</item> |         <item name="android:textAlignment">center</item> | ||||||
|  |         <item name="android:gravity">center</item> | ||||||
|         <item name="android:textSize">25.7sp</item> <!-- 46px * 96dpi / 160dpi --> |         <item name="android:textSize">25.7sp</item> <!-- 46px * 96dpi / 160dpi --> | ||||||
|         <item name="android:paddingTop">28dp</item> <!-- 50px * 96dpi / 160dpi --> |         <item name="android:paddingTop">28dp</item> <!-- 50px * 96dpi / 160dpi --> | ||||||
|         <item name="android:paddingBottom">20.1dp</item> <!-- 36px * 96dpi / 160dpi --> |         <item name="android:paddingBottom">20.1dp</item> <!-- 36px * 96dpi / 160dpi --> | ||||||
| @ -73,6 +74,7 @@ | |||||||
| 
 | 
 | ||||||
|     <style name="SwapTheme.AppList.SwapSuccessDetails"> |     <style name="SwapTheme.AppList.SwapSuccessDetails"> | ||||||
|         <item name="android:textAlignment">center</item> |         <item name="android:textAlignment">center</item> | ||||||
|  |         <item name="android:gravity">center</item> | ||||||
|         <item name="android:textSize">20.1sp</item> <!-- 36px * 96dpi / 160dpi --> |         <item name="android:textSize">20.1sp</item> <!-- 36px * 96dpi / 160dpi --> | ||||||
|         <item name="android:paddingTop">20.1dp</item> <!-- 36px * 96dpi / 160dpi --> |         <item name="android:paddingTop">20.1dp</item> <!-- 36px * 96dpi / 160dpi --> | ||||||
|         <item name="android:paddingBottom">20.1dp</item> <!-- 36px * 96dpi / 160dpi --> |         <item name="android:paddingBottom">20.1dp</item> <!-- 36px * 96dpi / 160dpi --> | ||||||
| @ -81,6 +83,7 @@ | |||||||
| 
 | 
 | ||||||
|     <style name="SwapTheme.StartSwap.MainText"> |     <style name="SwapTheme.StartSwap.MainText"> | ||||||
|         <item name="android:textAlignment">center</item> |         <item name="android:textAlignment">center</item> | ||||||
|  |         <item name="android:gravity">center</item> | ||||||
|         <item name="android:textSize">20.1sp</item> <!-- 36px * 96dpi / 160dpi --> |         <item name="android:textSize">20.1sp</item> <!-- 36px * 96dpi / 160dpi --> | ||||||
|         <item name="android:paddingLeft">28dp</item> <!-- 50px * 96dpi / 160dpi --> |         <item name="android:paddingLeft">28dp</item> <!-- 50px * 96dpi / 160dpi --> | ||||||
|         <item name="android:paddingRight">28dp</item> <!-- 50px * 96dpi / 160dpi --> |         <item name="android:paddingRight">28dp</item> <!-- 50px * 96dpi / 160dpi --> | ||||||
| @ -90,6 +93,7 @@ | |||||||
| 
 | 
 | ||||||
|     <style name="SwapTheme.Wizard.Text"> |     <style name="SwapTheme.Wizard.Text"> | ||||||
|         <item name="android:textAlignment">center</item> |         <item name="android:textAlignment">center</item> | ||||||
|  |         <item name="android:gravity">center</item> | ||||||
|         <item name="android:textColor">#fff</item> |         <item name="android:textColor">#fff</item> | ||||||
|         <item name="android:textColorPrimary">#fff</item> |         <item name="android:textColorPrimary">#fff</item> | ||||||
|         <item name="android:textColorSecondary">#fff</item> |         <item name="android:textColorSecondary">#fff</item> | ||||||
|  | |||||||
| @ -768,13 +768,17 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A | |||||||
|         startActivity(intent); |         startActivity(intent); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     protected void navigateUp() { | ||||||
|  |         NavUtils.navigateUpFromSameTask(this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public boolean onOptionsItemSelected(MenuItem item) { |     public boolean onOptionsItemSelected(MenuItem item) { | ||||||
| 
 | 
 | ||||||
|         switch (item.getItemId()) { |         switch (item.getItemId()) { | ||||||
| 
 | 
 | ||||||
|         case android.R.id.home: |         case android.R.id.home: | ||||||
|             NavUtils.navigateUpFromSameTask(this); |             navigateUp(); | ||||||
|             return true; |             return true; | ||||||
| 
 | 
 | ||||||
|         case LAUNCH: |         case LAUNCH: | ||||||
|  | |||||||
| @ -346,23 +346,28 @@ public class UpdateService extends IntentService implements ProgressListener { | |||||||
|             List<Repo> repos = RepoProvider.Helper.all(this); |             List<Repo> repos = RepoProvider.Helper.all(this); | ||||||
| 
 | 
 | ||||||
|             // Process each repo... |             // Process each repo... | ||||||
|             Map<String, App> appsToUpdate = new HashMap<String, App>(); |             Map<String, App> appsToUpdate = new HashMap<>(); | ||||||
|             List<Apk> apksToUpdate = new ArrayList<Apk>(); |             List<Apk> apksToUpdate = new ArrayList<>(); | ||||||
|             List<Repo> unchangedRepos = new ArrayList<Repo>(); |             List<Repo> swapRepos = new ArrayList<>(); | ||||||
|             List<Repo> updatedRepos = new ArrayList<Repo>(); |             List<Repo> unchangedRepos = new ArrayList<>(); | ||||||
|             List<Repo> disabledRepos = new ArrayList<Repo>(); |             List<Repo> updatedRepos = new ArrayList<>(); | ||||||
|             List<CharSequence> errorRepos = new ArrayList<CharSequence>(); |             List<Repo> disabledRepos = new ArrayList<>(); | ||||||
|             ArrayList<CharSequence> repoErrors = new ArrayList<CharSequence>(); |             List<CharSequence> errorRepos = new ArrayList<>(); | ||||||
|             List<RepoUpdater.RepoUpdateRememberer> repoUpdateRememberers = new ArrayList<RepoUpdater.RepoUpdateRememberer>(); |             ArrayList<CharSequence> repoErrors = new ArrayList<>(); | ||||||
|  |             List<RepoUpdater.RepoUpdateRememberer> repoUpdateRememberers = new ArrayList<>(); | ||||||
|             boolean changes = false; |             boolean changes = false; | ||||||
|  |             boolean singleRepoUpdate = !TextUtils.isEmpty(address); | ||||||
|             for (final Repo repo : repos) { |             for (final Repo repo : repos) { | ||||||
| 
 | 
 | ||||||
|                 if (!repo.inuse) { |                 if (!repo.inuse) { | ||||||
|                     disabledRepos.add(repo); |                     disabledRepos.add(repo); | ||||||
|                     continue; |                     continue; | ||||||
|                 } else if (!TextUtils.isEmpty(address) && !repo.address.equals(address)) { |                 } else if (singleRepoUpdate && !repo.address.equals(address)) { | ||||||
|                     unchangedRepos.add(repo); |                     unchangedRepos.add(repo); | ||||||
|                     continue; |                     continue; | ||||||
|  |                 } else if (!singleRepoUpdate && repo.isSwap) { | ||||||
|  |                     swapRepos.add(repo); | ||||||
|  |                     continue; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address)); |                 sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address)); | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ import android.content.pm.PackageManager.NameNotFoundException; | |||||||
| import android.content.res.AssetManager; | import android.content.res.AssetManager; | ||||||
| import android.content.res.XmlResourceParser; | import android.content.res.XmlResourceParser; | ||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
| import android.support.annotation.Nullable; | import android.support.annotation.Nullable; | ||||||
| import android.text.Editable; | import android.text.Editable; | ||||||
| import android.text.Html; | import android.text.Html; | ||||||
| @ -59,6 +60,9 @@ import java.util.Locale; | |||||||
| 
 | 
 | ||||||
| public final class Utils { | public final class Utils { | ||||||
| 
 | 
 | ||||||
|  |     @SuppressWarnings("UnusedDeclaration") | ||||||
|  |     private static final String TAG = "fdroid.Utils"; | ||||||
|  | 
 | ||||||
|     public static final int BUFFER_SIZE = 4096; |     public static final int BUFFER_SIZE = 4096; | ||||||
| 
 | 
 | ||||||
|     // The date format used for storing dates (e.g. lastupdated, added) in the |     // The date format used for storing dates (e.g. lastupdated, added) in the | ||||||
| @ -71,8 +75,6 @@ public final class Utils { | |||||||
|     public static final SimpleDateFormat LOG_DATE_FORMAT = |     public static final SimpleDateFormat LOG_DATE_FORMAT = | ||||||
|             new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); |             new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); | ||||||
| 
 | 
 | ||||||
|     private static final String TAG = "fdroid.Utils"; |  | ||||||
| 
 |  | ||||||
|     public static String getIconsDir(Context context) { |     public static String getIconsDir(Context context) { | ||||||
|         DisplayMetrics metrics = context.getResources().getDisplayMetrics(); |         DisplayMetrics metrics = context.getResources().getDisplayMetrics(); | ||||||
|         String iconsDir; |         String iconsDir; | ||||||
| @ -230,11 +232,7 @@ public final class Utils { | |||||||
|                 } |                 } | ||||||
|                 eventType = xml.nextToken(); |                 eventType = xml.nextToken(); | ||||||
|             } |             } | ||||||
|         } catch (NameNotFoundException e) { |         } catch (NameNotFoundException | IOException | XmlPullParserException e) { | ||||||
|             e.printStackTrace(); |  | ||||||
|         } catch (IOException e) { |  | ||||||
|             e.printStackTrace(); |  | ||||||
|         } catch (XmlPullParserException e) { |  | ||||||
|             e.printStackTrace(); |             e.printStackTrace(); | ||||||
|         } |         } | ||||||
|         return 8; // some kind of hopeful default |         return 8; // some kind of hopeful default | ||||||
| @ -282,7 +280,8 @@ public final class Utils { | |||||||
|         return displayFP; |         return displayFP; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static Uri getSharingUri(Context context, Repo repo) { |     @NonNull | ||||||
|  |     public static Uri getSharingUri(Repo repo) { | ||||||
|         if (TextUtils.isEmpty(repo.address)) |         if (TextUtils.isEmpty(repo.address)) | ||||||
|             return Uri.parse("http://wifi-not-enabled"); |             return Uri.parse("http://wifi-not-enabled"); | ||||||
|         Uri uri = Uri.parse(repo.address.replaceFirst("http", "fdroidrepo")); |         Uri uri = Uri.parse(repo.address.replaceFirst("http", "fdroidrepo")); | ||||||
| @ -345,8 +344,8 @@ public final class Utils { | |||||||
|             digest.update(key); |             digest.update(key); | ||||||
|             byte[] fingerprint = digest.digest(); |             byte[] fingerprint = digest.digest(); | ||||||
|             Formatter formatter = new Formatter(new StringBuilder()); |             Formatter formatter = new Formatter(new StringBuilder()); | ||||||
|             for (int i = 0; i < fingerprint.length; i++) { |             for (byte aFingerprint : fingerprint) { | ||||||
|                 formatter.format("%02X", fingerprint[i]); |                 formatter.format("%02X", aFingerprint); | ||||||
|             } |             } | ||||||
|             ret = formatter.toString(); |             ret = formatter.toString(); | ||||||
|             formatter.close(); |             formatter.close(); | ||||||
| @ -432,14 +431,14 @@ public final class Utils { | |||||||
| 
 | 
 | ||||||
|     public static String getBinaryHash(File apk, String algo) { |     public static String getBinaryHash(File apk, String algo) { | ||||||
|         FileInputStream fis = null; |         FileInputStream fis = null; | ||||||
|         BufferedInputStream bis = null; |         BufferedInputStream bis; | ||||||
|         try { |         try { | ||||||
|             MessageDigest md = MessageDigest.getInstance(algo); |             MessageDigest md = MessageDigest.getInstance(algo); | ||||||
|             fis = new FileInputStream(apk); |             fis = new FileInputStream(apk); | ||||||
|             bis = new BufferedInputStream(fis); |             bis = new BufferedInputStream(fis); | ||||||
| 
 | 
 | ||||||
|             byte[] dataBytes = new byte[524288]; |             byte[] dataBytes = new byte[524288]; | ||||||
|             int nread = 0; |             int nread; | ||||||
| 
 | 
 | ||||||
|             while ((nread = bis.read(dataBytes)) != -1) |             while ((nread = bis.read(dataBytes)) != -1) | ||||||
|                 md.update(dataBytes, 0, nread); |                 md.update(dataBytes, 0, nread); | ||||||
| @ -483,7 +482,7 @@ public final class Utils { | |||||||
|                     listNum = -1; |                     listNum = -1; | ||||||
|                 else |                 else | ||||||
|                     output.append('\n'); |                     output.append('\n'); | ||||||
|             } else if (opening && tag.equals("ol")) { |             } else if (tag.equals("ol")) { | ||||||
|                 if (opening) |                 if (opening) | ||||||
|                     listNum = 1; |                     listNum = 1; | ||||||
|                 else |                 else | ||||||
|  | |||||||
| @ -259,10 +259,17 @@ public class AppProvider extends FDroidProvider { | |||||||
|         private boolean isSuggestedApkTableAdded = false; |         private boolean isSuggestedApkTableAdded = false; | ||||||
|         private boolean requiresInstalledTable = false; |         private boolean requiresInstalledTable = false; | ||||||
|         private boolean categoryFieldAdded = false; |         private boolean categoryFieldAdded = false; | ||||||
|  |         private boolean countFieldAppended = false; | ||||||
| 
 | 
 | ||||||
|         @Override |         @Override | ||||||
|         protected String getRequiredTables() { |         protected String getRequiredTables() { | ||||||
|             return DBHelper.TABLE_APP; |             final String app  = DBHelper.TABLE_APP; | ||||||
|  |             final String apk  = DBHelper.TABLE_APK; | ||||||
|  |             final String repo = DBHelper.TABLE_REPO; | ||||||
|  | 
 | ||||||
|  |             return app + | ||||||
|  |                 " LEFT JOIN " + apk + " ON ( " + apk + ".id = " + app + ".id ) " + | ||||||
|  |                 " LEFT JOIN " + repo + " ON ( " + apk + ".repo = " + repo + "._id )"; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         @Override |         @Override | ||||||
| @ -270,6 +277,12 @@ public class AppProvider extends FDroidProvider { | |||||||
|             return fieldCount() == 1 && categoryFieldAdded; |             return fieldCount() == 1 && categoryFieldAdded; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         @Override | ||||||
|  |         protected String groupBy() { | ||||||
|  |             // If the count field has been requested, then we want to group all rows together. | ||||||
|  |             return countFieldAppended ? null : DBHelper.TABLE_APP + ".id"; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         public void addSelection(AppQuerySelection selection) { |         public void addSelection(AppQuerySelection selection) { | ||||||
|             addSelection(selection.getSelection()); |             addSelection(selection.getSelection()); | ||||||
|             if (selection.naturalJoinToInstalled()) { |             if (selection.naturalJoinToInstalled()) { | ||||||
| @ -318,7 +331,8 @@ public class AppProvider extends FDroidProvider { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private void appendCountField() { |         private void appendCountField() { | ||||||
|             appendField("COUNT(*) AS " + DataColumns._COUNT); |             countFieldAppended = true; | ||||||
|  |             appendField("COUNT( DISTINCT fdroid_app.id ) AS " + DataColumns._COUNT); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private void addSuggestedApkVersionField() { |         private void addSuggestedApkVersionField() { | ||||||
| @ -372,6 +386,7 @@ public class AppProvider extends FDroidProvider { | |||||||
|     private static final String PATH_CATEGORY = "category"; |     private static final String PATH_CATEGORY = "category"; | ||||||
|     private static final String PATH_IGNORED = "ignored"; |     private static final String PATH_IGNORED = "ignored"; | ||||||
|     private static final String PATH_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex"; |     private static final String PATH_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex"; | ||||||
|  |     private static final String PATH_REPO = "repo"; | ||||||
| 
 | 
 | ||||||
|     private static final int CAN_UPDATE       = CODE_SINGLE + 1; |     private static final int CAN_UPDATE       = CODE_SINGLE + 1; | ||||||
|     private static final int INSTALLED        = CAN_UPDATE + 1; |     private static final int INSTALLED        = CAN_UPDATE + 1; | ||||||
| @ -383,6 +398,7 @@ public class AppProvider extends FDroidProvider { | |||||||
|     private static final int CATEGORY         = NEWLY_ADDED + 1; |     private static final int CATEGORY         = NEWLY_ADDED + 1; | ||||||
|     private static final int IGNORED          = CATEGORY + 1; |     private static final int IGNORED          = CATEGORY + 1; | ||||||
|     private static final int CALC_APP_DETAILS_FROM_INDEX = IGNORED + 1; |     private static final int CALC_APP_DETAILS_FROM_INDEX = IGNORED + 1; | ||||||
|  |     private static final int REPO             = CALC_APP_DETAILS_FROM_INDEX + 1; | ||||||
| 
 | 
 | ||||||
|     static { |     static { | ||||||
|         matcher.addURI(getAuthority(), null, CODE_LIST); |         matcher.addURI(getAuthority(), null, CODE_LIST); | ||||||
| @ -392,6 +408,7 @@ public class AppProvider extends FDroidProvider { | |||||||
|         matcher.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED); |         matcher.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED); | ||||||
|         matcher.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY); |         matcher.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY); | ||||||
|         matcher.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH); |         matcher.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH); | ||||||
|  |         matcher.addURI(getAuthority(), PATH_REPO + "/#", REPO); | ||||||
|         matcher.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE); |         matcher.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE); | ||||||
|         matcher.addURI(getAuthority(), PATH_INSTALLED, INSTALLED); |         matcher.addURI(getAuthority(), PATH_INSTALLED, INSTALLED); | ||||||
|         matcher.addURI(getAuthority(), PATH_NO_APKS, NO_APKS); |         matcher.addURI(getAuthority(), PATH_NO_APKS, NO_APKS); | ||||||
| @ -438,6 +455,13 @@ public class AppProvider extends FDroidProvider { | |||||||
|         return Uri.withAppendedPath(getContentUri(), PATH_CAN_UPDATE); |         return Uri.withAppendedPath(getContentUri(), PATH_CAN_UPDATE); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static Uri getRepoUri(Repo repo) { | ||||||
|  |         return getContentUri().buildUpon() | ||||||
|  |             .appendPath(PATH_REPO) | ||||||
|  |             .appendPath(String.valueOf(repo.id)) | ||||||
|  |             .build(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public static Uri getContentUri(List<App> apps) { |     public static Uri getContentUri(List<App> apps) { | ||||||
|         StringBuilder builder = new StringBuilder(); |         StringBuilder builder = new StringBuilder(); | ||||||
|         for (int i = 0; i < apps.size(); i ++) { |         for (int i = 0; i < apps.size(); i ++) { | ||||||
| @ -494,6 +518,12 @@ public class AppProvider extends FDroidProvider { | |||||||
|         return new AppQuerySelection(where).requireNaturalInstalledTable(); |         return new AppQuerySelection(where).requireNaturalInstalledTable(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private AppQuerySelection queryRepo(long repoId) { | ||||||
|  |         String selection = " fdroid_apk.repo = ? "; | ||||||
|  |         String[] args = { String.valueOf(repoId) }; | ||||||
|  |         return new AppQuerySelection(selection, args); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private AppQuerySelection queryInstalled() { |     private AppQuerySelection queryInstalled() { | ||||||
|         return new AppQuerySelection().requireNaturalInstalledTable(); |         return new AppQuerySelection().requireNaturalInstalledTable(); | ||||||
|     } |     } | ||||||
| @ -555,6 +585,14 @@ public class AppProvider extends FDroidProvider { | |||||||
|         return new AppQuerySelection(selection); |         return new AppQuerySelection(selection); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private AppQuerySelection queryExcludeSwap() { | ||||||
|  |         // fdroid_repo will have null fields if the LEFT JOIN didn't resolve, e.g. due to there | ||||||
|  |         // being no apks for the app in the result set. In that case, we can't tell if it is from | ||||||
|  |         // a swap repo or not. | ||||||
|  |         String selection = " fdroid_repo.isSwap = 0 OR fdroid_repo.isSwap is null "; | ||||||
|  |         return new AppQuerySelection(selection); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private AppQuerySelection queryNewlyAdded() { |     private AppQuerySelection queryNewlyAdded() { | ||||||
|         String selection = "fdroid_app.added > ?"; |         String selection = "fdroid_app.added > ?"; | ||||||
|         String[] args = { Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) }; |         String[] args = { Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) }; | ||||||
| @ -599,8 +637,13 @@ public class AppProvider extends FDroidProvider { | |||||||
|     public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) { |     public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) { | ||||||
|         Query query = new Query(); |         Query query = new Query(); | ||||||
|         AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs); |         AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs); | ||||||
|  | 
 | ||||||
|  |         // Queries which are for the main list of apps should not include swap apps. | ||||||
|  |         boolean includeSwap = true; | ||||||
|  | 
 | ||||||
|         switch (matcher.match(uri)) { |         switch (matcher.match(uri)) { | ||||||
|             case CODE_LIST: |             case CODE_LIST: | ||||||
|  |                 includeSwap = false; | ||||||
|                 break; |                 break; | ||||||
| 
 | 
 | ||||||
|             case CODE_SINGLE: |             case CODE_SINGLE: | ||||||
| @ -609,14 +652,21 @@ public class AppProvider extends FDroidProvider { | |||||||
| 
 | 
 | ||||||
|             case CAN_UPDATE: |             case CAN_UPDATE: | ||||||
|                 selection = selection.add(queryCanUpdate()); |                 selection = selection.add(queryCanUpdate()); | ||||||
|  |                 includeSwap = false; | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             case REPO: | ||||||
|  |                 selection = selection.add(queryRepo(Long.parseLong(uri.getLastPathSegment()))); | ||||||
|                 break; |                 break; | ||||||
| 
 | 
 | ||||||
|             case INSTALLED: |             case INSTALLED: | ||||||
|                 selection = selection.add(queryInstalled()); |                 selection = selection.add(queryInstalled()); | ||||||
|  |                 includeSwap = false; | ||||||
|                 break; |                 break; | ||||||
| 
 | 
 | ||||||
|             case SEARCH: |             case SEARCH: | ||||||
|                 selection = selection.add(querySearch(uri.getLastPathSegment())); |                 selection = selection.add(querySearch(uri.getLastPathSegment())); | ||||||
|  |                 includeSwap = false; | ||||||
|                 break; |                 break; | ||||||
| 
 | 
 | ||||||
|             case NO_APKS: |             case NO_APKS: | ||||||
| @ -633,16 +683,19 @@ public class AppProvider extends FDroidProvider { | |||||||
| 
 | 
 | ||||||
|             case CATEGORY: |             case CATEGORY: | ||||||
|                 selection = selection.add(queryCategory(uri.getLastPathSegment())); |                 selection = selection.add(queryCategory(uri.getLastPathSegment())); | ||||||
|  |                 includeSwap = false; | ||||||
|                 break; |                 break; | ||||||
| 
 | 
 | ||||||
|             case RECENTLY_UPDATED: |             case RECENTLY_UPDATED: | ||||||
|                 sortOrder = " fdroid_app.lastUpdated DESC"; |                 sortOrder = " fdroid_app.lastUpdated DESC"; | ||||||
|                 selection = selection.add(queryRecentlyUpdated()); |                 selection = selection.add(queryRecentlyUpdated()); | ||||||
|  |                 includeSwap = false; | ||||||
|                 break; |                 break; | ||||||
| 
 | 
 | ||||||
|             case NEWLY_ADDED: |             case NEWLY_ADDED: | ||||||
|                 sortOrder = " fdroid_app.added DESC"; |                 sortOrder = " fdroid_app.added DESC"; | ||||||
|                 selection = selection.add(queryNewlyAdded()); |                 selection = selection.add(queryNewlyAdded()); | ||||||
|  |                 includeSwap = false; | ||||||
|                 break; |                 break; | ||||||
| 
 | 
 | ||||||
|             default: |             default: | ||||||
| @ -650,6 +703,10 @@ public class AppProvider extends FDroidProvider { | |||||||
|                 throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri); |                 throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         if (!includeSwap) { | ||||||
|  |             selection = selection.add(queryExcludeSwap()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         if (AppProvider.DataColumns.NAME.equals(sortOrder)) { |         if (AppProvider.DataColumns.NAME.equals(sortOrder)) { | ||||||
|             sortOrder = " lower( fdroid_app." + sortOrder + " ) "; |             sortOrder = " lower( fdroid_app." + sortOrder + " ) "; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -33,7 +33,8 @@ public class DBHelper extends SQLiteOpenHelper { | |||||||
|             + "priority integer not null, pubkey text, fingerprint text, " |             + "priority integer not null, pubkey text, fingerprint text, " | ||||||
|             + "maxage integer not null default 0, " |             + "maxage integer not null default 0, " | ||||||
|             + "version integer not null default 0, " |             + "version integer not null default 0, " | ||||||
|             + "lastetag text, lastUpdated string);"; |             + "lastetag text, lastUpdated string," | ||||||
|  |             + "isSwap integer boolean default 0);"; | ||||||
| 
 | 
 | ||||||
|     private static final String CREATE_TABLE_APK = |     private static final String CREATE_TABLE_APK = | ||||||
|             "CREATE TABLE " + TABLE_APK + " ( " |             "CREATE TABLE " + TABLE_APK + " ( " | ||||||
| @ -98,7 +99,7 @@ public class DBHelper extends SQLiteOpenHelper { | |||||||
|             + InstalledAppProvider.DataColumns.APPLICATION_LABEL + " TEXT NOT NULL " |             + InstalledAppProvider.DataColumns.APPLICATION_LABEL + " TEXT NOT NULL " | ||||||
|             + " );"; |             + " );"; | ||||||
| 
 | 
 | ||||||
|     private static final int DB_VERSION = 46; |     private static final int DB_VERSION = 47; | ||||||
| 
 | 
 | ||||||
|     private Context context; |     private Context context; | ||||||
| 
 | 
 | ||||||
| @ -273,6 +274,7 @@ public class DBHelper extends SQLiteOpenHelper { | |||||||
|         populateRepoNames(db, oldVersion); |         populateRepoNames(db, oldVersion); | ||||||
|         if (oldVersion < 43) createInstalledApp(db); |         if (oldVersion < 43) createInstalledApp(db); | ||||||
|         addAppLabelToInstalledCache(db, oldVersion); |         addAppLabelToInstalledCache(db, oldVersion); | ||||||
|  |         addIsSwapToRepo(db, oldVersion); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -400,6 +402,13 @@ public class DBHelper extends SQLiteOpenHelper { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private void addIsSwapToRepo(SQLiteDatabase db, int oldVersion) { | ||||||
|  |         if (oldVersion < 47 && !columnExists(db, TABLE_REPO, "isSwap")) { | ||||||
|  |             Log.i(TAG, "Adding isSwap field to " + TABLE_REPO + " table in db."); | ||||||
|  |             db.execSQL("alter table " + TABLE_REPO + " add column isSwap boolean default 0;"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private void resetTransient(SQLiteDatabase db, int oldVersion) { |     private void resetTransient(SQLiteDatabase db, int oldVersion) { | ||||||
|         // Before version 42, only transient info was stored in here. As of some time |         // Before version 42, only transient info was stored in here. As of some time | ||||||
|         // just before 42 (F-Droid 0.60ish) it now has "ignore this version" info which |         // just before 42 (F-Droid 0.60ish) it now has "ignore this version" info which | ||||||
|  | |||||||
| @ -28,6 +28,10 @@ abstract class QueryBuilder { | |||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     protected String groupBy() { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     protected void appendField(String field) { |     protected void appendField(String field) { | ||||||
|         appendField(field, null, null); |         appendField(field, null, null); | ||||||
|     } |     } | ||||||
| @ -85,6 +89,10 @@ abstract class QueryBuilder { | |||||||
|             .append(')'); |             .append(')'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private String distinctSql() { | ||||||
|  |         return isDistinct() ? " DISTINCT " : ""; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private String fieldsSql() { |     private String fieldsSql() { | ||||||
|         StringBuilder sb = new StringBuilder(); |         StringBuilder sb = new StringBuilder(); | ||||||
|         for (int i = 0; i < fields.size(); i ++) { |         for (int i = 0; i < fields.size(); i ++) { | ||||||
| @ -104,12 +112,15 @@ abstract class QueryBuilder { | |||||||
|         return orderBy != null ? " ORDER BY " + orderBy : ""; |         return orderBy != null ? " ORDER BY " + orderBy : ""; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private String groupBySql() { | ||||||
|  |         return groupBy() != null ? " GROUP BY " + groupBy() : ""; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private String tablesSql() { |     private String tablesSql() { | ||||||
|         return tables.toString(); |         return tables.toString(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public String toString() { |     public String toString() { | ||||||
|         String distinct = isDistinct() ? " DISTINCT " : ""; |         return "SELECT " + distinctSql() + fieldsSql() + " FROM " + tablesSql() + whereSql() + groupBySql() + orderBySql(); | ||||||
|         return "SELECT " + distinct + fieldsSql() + " FROM " + tablesSql() + whereSql() + orderBySql(); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -30,6 +30,7 @@ public class Repo extends ValueObject { | |||||||
|     public int maxage; // maximum age of index that will be accepted - 0 for any |     public int maxage; // maximum age of index that will be accepted - 0 for any | ||||||
|     public String lastetag; // last etag we updated from, null forces update |     public String lastetag; // last etag we updated from, null forces update | ||||||
|     public Date lastUpdated; |     public Date lastUpdated; | ||||||
|  |     public boolean isSwap; | ||||||
| 
 | 
 | ||||||
|     public Repo() { |     public Repo() { | ||||||
| 
 | 
 | ||||||
| @ -65,6 +66,8 @@ public class Repo extends ValueObject { | |||||||
|                 pubkey = cursor.getString(i); |                 pubkey = cursor.getString(i); | ||||||
|             } else if (column.equals(RepoProvider.DataColumns.PRIORITY)) { |             } else if (column.equals(RepoProvider.DataColumns.PRIORITY)) { | ||||||
|                 priority = cursor.getInt(i); |                 priority = cursor.getInt(i); | ||||||
|  |             } else if (column.equals(RepoProvider.DataColumns.IS_SWAP)) { | ||||||
|  |                 isSwap = cursor.getInt(i) == 1; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -140,7 +143,7 @@ public class Repo extends ValueObject { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (values.containsKey(RepoProvider.DataColumns.IN_USE)) { |         if (values.containsKey(RepoProvider.DataColumns.IN_USE)) { | ||||||
|             inuse = toInt(values.getAsInteger(RepoProvider.DataColumns.FINGERPRINT)) == 1; |             inuse = toInt(values.getAsInteger(RepoProvider.DataColumns.IN_USE)) == 1; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (values.containsKey(RepoProvider.DataColumns.LAST_UPDATED)) { |         if (values.containsKey(RepoProvider.DataColumns.LAST_UPDATED)) { | ||||||
| @ -173,5 +176,9 @@ public class Repo extends ValueObject { | |||||||
|         if (values.containsKey(RepoProvider.DataColumns.PRIORITY)) { |         if (values.containsKey(RepoProvider.DataColumns.PRIORITY)) { | ||||||
|             priority = toInt(values.getAsInteger(RepoProvider.DataColumns.PRIORITY)); |             priority = toInt(values.getAsInteger(RepoProvider.DataColumns.PRIORITY)); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         if (values.containsKey(RepoProvider.DataColumns.IS_SWAP)) { | ||||||
|  |             isSwap= toInt(values.getAsInteger(RepoProvider.DataColumns.IS_SWAP)) == 1; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -218,19 +218,24 @@ public class RepoProvider extends FDroidProvider { | |||||||
|         public static String LAST_ETAG    = "lastetag"; |         public static String LAST_ETAG    = "lastetag"; | ||||||
|         public static String LAST_UPDATED = "lastUpdated"; |         public static String LAST_UPDATED = "lastUpdated"; | ||||||
|         public static String VERSION      = "version"; |         public static String VERSION      = "version"; | ||||||
|  |         public static String IS_SWAP      = "isSwap"; | ||||||
| 
 | 
 | ||||||
|         public static String[] ALL = { |         public static String[] ALL = { | ||||||
|             _ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, PUBLIC_KEY, |             _ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, PUBLIC_KEY, | ||||||
|             FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION |             FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION, IS_SWAP | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static final String PROVIDER_NAME = "RepoProvider"; |     private static final String PROVIDER_NAME = "RepoProvider"; | ||||||
|  |     private static final String PATH_ALL_EXCEPT_SWAP = "allExceptSwap"; | ||||||
|  | 
 | ||||||
|  |     private static final int CODE_ALL_EXCEPT_SWAP = CODE_SINGLE + 1; | ||||||
| 
 | 
 | ||||||
|     private static final UriMatcher matcher = new UriMatcher(-1); |     private static final UriMatcher matcher = new UriMatcher(-1); | ||||||
| 
 | 
 | ||||||
|     static { |     static { | ||||||
|         matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, null, CODE_LIST); |         matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, null, CODE_LIST); | ||||||
|  |         matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, PATH_ALL_EXCEPT_SWAP, CODE_ALL_EXCEPT_SWAP); | ||||||
|         matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "#", CODE_SINGLE); |         matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "#", CODE_SINGLE); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -242,6 +247,12 @@ public class RepoProvider extends FDroidProvider { | |||||||
|         return ContentUris.withAppendedId(getContentUri(), repoId); |         return ContentUris.withAppendedId(getContentUri(), repoId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static Uri allExceptSwapUri() { | ||||||
|  |         return getContentUri().buildUpon() | ||||||
|  |                 .appendPath(PATH_ALL_EXCEPT_SWAP) | ||||||
|  |                 .build(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     protected String getTableName() { |     protected String getTableName() { | ||||||
|         return DBHelper.TABLE_REPO; |         return DBHelper.TABLE_REPO; | ||||||
| @ -260,11 +271,13 @@ public class RepoProvider extends FDroidProvider { | |||||||
|     @Override |     @Override | ||||||
|     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { |     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { | ||||||
| 
 | 
 | ||||||
|  |         if (TextUtils.isEmpty(sortOrder)) { | ||||||
|  |             sortOrder = "_ID ASC"; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         switch (matcher.match(uri)) { |         switch (matcher.match(uri)) { | ||||||
|             case CODE_LIST: |             case CODE_LIST: | ||||||
|                 if (TextUtils.isEmpty(sortOrder)) { |                 // Do nothing (don't restrict query) | ||||||
|                     sortOrder = "_ID ASC"; |  | ||||||
|                 } |  | ||||||
|                 break; |                 break; | ||||||
| 
 | 
 | ||||||
|             case CODE_SINGLE: |             case CODE_SINGLE: | ||||||
| @ -272,6 +285,10 @@ public class RepoProvider extends FDroidProvider { | |||||||
|                         DataColumns._ID + " = " + uri.getLastPathSegment(); |                         DataColumns._ID + " = " + uri.getLastPathSegment(); | ||||||
|                 break; |                 break; | ||||||
| 
 | 
 | ||||||
|  |             case CODE_ALL_EXCEPT_SWAP: | ||||||
|  |                 selection = DataColumns.IS_SWAP + " = 0 OR " + DataColumns.IS_SWAP + " IS NULL "; | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|             default: |             default: | ||||||
|                 Log.e(TAG, "Invalid URI for repo content provider: " + uri); |                 Log.e(TAG, "Invalid URI for repo content provider: " + uri); | ||||||
|                 throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri); |                 throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri); | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| package org.fdroid.fdroid.localrepo; | package org.fdroid.fdroid.localrepo; | ||||||
| 
 | 
 | ||||||
| import android.annotation.TargetApi; |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.SharedPreferences; | import android.content.SharedPreferences; | ||||||
| import android.content.pm.ApplicationInfo; | import android.content.pm.ApplicationInfo; | ||||||
| @ -15,27 +14,23 @@ import android.graphics.Canvas; | |||||||
| import android.graphics.drawable.BitmapDrawable; | import android.graphics.drawable.BitmapDrawable; | ||||||
| import android.graphics.drawable.Drawable; | import android.graphics.drawable.Drawable; | ||||||
| import android.preference.PreferenceManager; | import android.preference.PreferenceManager; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
| import android.text.TextUtils; | import android.text.TextUtils; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.widget.Toast; | import android.widget.Toast; | ||||||
| 
 | 
 | ||||||
|  | import org.fdroid.fdroid.FDroidApp; | ||||||
| import org.fdroid.fdroid.Hasher; | import org.fdroid.fdroid.Hasher; | ||||||
| import org.fdroid.fdroid.Preferences; | import org.fdroid.fdroid.Preferences; | ||||||
| import org.fdroid.fdroid.R; | import org.fdroid.fdroid.R; | ||||||
| import org.fdroid.fdroid.Utils; | import org.fdroid.fdroid.Utils; | ||||||
| import org.fdroid.fdroid.data.App; | import org.fdroid.fdroid.data.App; | ||||||
| import org.fdroid.fdroid.data.SanitizedFile; | import org.fdroid.fdroid.data.SanitizedFile; | ||||||
| import org.w3c.dom.Document; | import org.xmlpull.v1.XmlPullParserException; | ||||||
| import org.w3c.dom.Element; | import org.xmlpull.v1.XmlPullParserFactory; | ||||||
|  | import org.xmlpull.v1.XmlSerializer; | ||||||
| 
 | 
 | ||||||
| import javax.xml.parsers.DocumentBuilder; |  | ||||||
| import javax.xml.parsers.DocumentBuilderFactory; |  | ||||||
| import javax.xml.parsers.ParserConfigurationException; |  | ||||||
| import javax.xml.transform.Transformer; |  | ||||||
| import javax.xml.transform.TransformerException; |  | ||||||
| import javax.xml.transform.TransformerFactory; |  | ||||||
| import javax.xml.transform.dom.DOMSource; |  | ||||||
| import javax.xml.transform.stream.StreamResult; |  | ||||||
| import java.io.BufferedInputStream; | import java.io.BufferedInputStream; | ||||||
| import java.io.BufferedOutputStream; | import java.io.BufferedOutputStream; | ||||||
| import java.io.BufferedReader; | import java.io.BufferedReader; | ||||||
| @ -43,13 +38,17 @@ import java.io.BufferedWriter; | |||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.FileInputStream; | import java.io.FileInputStream; | ||||||
| import java.io.FileOutputStream; | import java.io.FileOutputStream; | ||||||
|  | import java.io.FileWriter; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.InputStreamReader; | import java.io.InputStreamReader; | ||||||
| import java.io.OutputStream; | import java.io.OutputStream; | ||||||
| import java.io.OutputStreamWriter; | import java.io.OutputStreamWriter; | ||||||
|  | import java.io.Writer; | ||||||
| import java.security.cert.CertificateEncodingException; | import java.security.cert.CertificateEncodingException; | ||||||
|  | import java.text.DateFormat; | ||||||
| import java.text.SimpleDateFormat; | import java.text.SimpleDateFormat; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
|  | import java.util.Date; | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Locale; | import java.util.Locale; | ||||||
| @ -67,30 +66,30 @@ public class LocalRepoManager { | |||||||
|     private final Context context; |     private final Context context; | ||||||
|     private final PackageManager pm; |     private final PackageManager pm; | ||||||
|     private final AssetManager assetManager; |     private final AssetManager assetManager; | ||||||
|     private final SharedPreferences prefs; |  | ||||||
|     private final String fdroidPackageName; |     private final String fdroidPackageName; | ||||||
| 
 | 
 | ||||||
|     private String ipAddressString = "UNSET"; |  | ||||||
|     private String uriString = "UNSET"; |  | ||||||
| 
 |  | ||||||
|     private static String[] WEB_ROOT_ASSET_FILES = { |     private static String[] WEB_ROOT_ASSET_FILES = { | ||||||
|         "swap-icon.png", |         "swap-icon.png", | ||||||
|         "swap-tick-done.png", |         "swap-tick-done.png", | ||||||
|         "swap-tick-not-done.png" |         "swap-tick-not-done.png" | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     private Map<String, App> apps = new HashMap<String, App>(); |     private Map<String, App> apps = new HashMap<>(); | ||||||
| 
 | 
 | ||||||
|     public final SanitizedFile xmlIndex; |     public final SanitizedFile xmlIndex; | ||||||
|     private SanitizedFile xmlIndexJar = null; |     private SanitizedFile xmlIndexJar = null; | ||||||
|     private SanitizedFile xmlIndexJarUnsigned = null; |     private SanitizedFile xmlIndexJarUnsigned = null; | ||||||
|     public final SanitizedFile webRoot; |     public final SanitizedFile webRoot; | ||||||
|     public final SanitizedFile fdroidDir; |     public final SanitizedFile fdroidDir; | ||||||
|  |     public final SanitizedFile fdroidDirCaps; | ||||||
|     public final SanitizedFile repoDir; |     public final SanitizedFile repoDir; | ||||||
|  |     public final SanitizedFile repoDirCaps; | ||||||
|     public final SanitizedFile iconsDir; |     public final SanitizedFile iconsDir; | ||||||
| 
 | 
 | ||||||
|  |     @Nullable | ||||||
|     private static LocalRepoManager localRepoManager; |     private static LocalRepoManager localRepoManager; | ||||||
| 
 | 
 | ||||||
|  |     @NonNull | ||||||
|     public static LocalRepoManager get(Context context) { |     public static LocalRepoManager get(Context context) { | ||||||
|         if (localRepoManager == null) |         if (localRepoManager == null) | ||||||
|             localRepoManager = new LocalRepoManager(context); |             localRepoManager = new LocalRepoManager(context); | ||||||
| @ -101,13 +100,14 @@ public class LocalRepoManager { | |||||||
|         context = c.getApplicationContext(); |         context = c.getApplicationContext(); | ||||||
|         pm = c.getPackageManager(); |         pm = c.getPackageManager(); | ||||||
|         assetManager = c.getAssets(); |         assetManager = c.getAssets(); | ||||||
|         prefs = PreferenceManager.getDefaultSharedPreferences(c); |  | ||||||
|         fdroidPackageName = c.getPackageName(); |         fdroidPackageName = c.getPackageName(); | ||||||
| 
 | 
 | ||||||
|         webRoot = SanitizedFile.knownSanitized(c.getFilesDir()); |         webRoot = SanitizedFile.knownSanitized(c.getFilesDir()); | ||||||
|         /* /fdroid/repo is the standard path for user repos */ |         /* /fdroid/repo is the standard path for user repos */ | ||||||
|         fdroidDir = new SanitizedFile(webRoot, "fdroid"); |         fdroidDir = new SanitizedFile(webRoot, "fdroid"); | ||||||
|  |         fdroidDirCaps = new SanitizedFile(webRoot, "FDROID"); | ||||||
|         repoDir = new SanitizedFile(fdroidDir, "repo"); |         repoDir = new SanitizedFile(fdroidDir, "repo"); | ||||||
|  |         repoDirCaps = new SanitizedFile(fdroidDirCaps, "REPO"); | ||||||
|         iconsDir = new SanitizedFile(repoDir, "icons"); |         iconsDir = new SanitizedFile(repoDir, "icons"); | ||||||
|         xmlIndex = new SanitizedFile(repoDir, "index.xml"); |         xmlIndex = new SanitizedFile(repoDir, "index.xml"); | ||||||
|         xmlIndexJar = new SanitizedFile(repoDir, "index.jar"); |         xmlIndexJar = new SanitizedFile(repoDir, "index.jar"); | ||||||
| @ -126,10 +126,6 @@ public class LocalRepoManager { | |||||||
|                 Log.e(TAG, "Unable to create icons folder: " + iconsDir); |                 Log.e(TAG, "Unable to create icons folder: " + iconsDir); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void setUriString(String uriString) { |  | ||||||
|         this.uriString = uriString; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private String writeFdroidApkToWebroot() { |     private String writeFdroidApkToWebroot() { | ||||||
|         ApplicationInfo appInfo; |         ApplicationInfo appInfo; | ||||||
|         String fdroidClientURL = "https://f-droid.org/FDroid.apk"; |         String fdroidClientURL = "https://f-droid.org/FDroid.apk"; | ||||||
| @ -138,7 +134,7 @@ public class LocalRepoManager { | |||||||
|             appInfo = pm.getApplicationInfo(fdroidPackageName, PackageManager.GET_META_DATA); |             appInfo = pm.getApplicationInfo(fdroidPackageName, PackageManager.GET_META_DATA); | ||||||
|             SanitizedFile apkFile = SanitizedFile.knownSanitized(appInfo.publicSourceDir); |             SanitizedFile apkFile = SanitizedFile.knownSanitized(appInfo.publicSourceDir); | ||||||
|             SanitizedFile fdroidApkLink = new SanitizedFile(webRoot, "fdroid.client.apk"); |             SanitizedFile fdroidApkLink = new SanitizedFile(webRoot, "fdroid.client.apk"); | ||||||
|             fdroidApkLink.delete(); |             attemptToDelete(fdroidApkLink); | ||||||
|             if (Utils.symlinkOrCopyFile(apkFile, fdroidApkLink)) |             if (Utils.symlinkOrCopyFile(apkFile, fdroidApkLink)) | ||||||
|                 fdroidClientURL = "/" + fdroidApkLink.getName(); |                 fdroidClientURL = "/" + fdroidApkLink.getName(); | ||||||
|         } catch (NameNotFoundException e) { |         } catch (NameNotFoundException e) { | ||||||
| @ -171,18 +167,15 @@ public class LocalRepoManager { | |||||||
| 
 | 
 | ||||||
|             // make symlinks/copies in each subdir of the repo to make sure that |             // make symlinks/copies in each subdir of the repo to make sure that | ||||||
|             // the user will always find the bootstrap page. |             // the user will always find the bootstrap page. | ||||||
|             symlinkIndexPageElsewhere("../", fdroidDir); |             symlinkEntireWebRootElsewhere("../", fdroidDir); | ||||||
|             symlinkIndexPageElsewhere("../../", repoDir); |             symlinkEntireWebRootElsewhere("../../", repoDir); | ||||||
| 
 | 
 | ||||||
|             // add in /FDROID/REPO to support bad QR Scanner apps |             // add in /FDROID/REPO to support bad QR Scanner apps | ||||||
|             File fdroidCAPS = new File(fdroidDir.getParentFile(), "FDROID"); |             attemptToMkdir(fdroidDirCaps); | ||||||
|             fdroidCAPS.mkdir(); |             attemptToMkdir(repoDirCaps); | ||||||
| 
 | 
 | ||||||
|             File repoCAPS = new File(fdroidCAPS, "REPO"); |             symlinkEntireWebRootElsewhere("../", fdroidDirCaps); | ||||||
|             repoCAPS.mkdir(); |             symlinkEntireWebRootElsewhere("../../", repoDirCaps); | ||||||
| 
 |  | ||||||
|             symlinkIndexPageElsewhere("../", fdroidCAPS); |  | ||||||
|             symlinkIndexPageElsewhere("../../", repoCAPS); |  | ||||||
| 
 | 
 | ||||||
|         } catch (IOException e) { |         } catch (IOException e) { | ||||||
|             Log.e(TAG, "Error writing local repo index: " + e.getMessage()); |             Log.e(TAG, "Error writing local repo index: " + e.getMessage()); | ||||||
| @ -190,16 +183,37 @@ public class LocalRepoManager { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void symlinkIndexPageElsewhere(String symlinkPrefix, File directory) { |     private static void attemptToMkdir(@NonNull File dir) throws IOException { | ||||||
|         SanitizedFile index = new SanitizedFile(directory, "index.html"); |         if (dir.exists()) { | ||||||
|         index.delete(); |             if (dir.isDirectory()) { | ||||||
|         Utils.symlinkOrCopyFile(new SanitizedFile(new File(symlinkPrefix), "index.html"), index); |                 return; | ||||||
| 
 |             } else { | ||||||
|         for(String fileName : WEB_ROOT_ASSET_FILES) { |                 throw new IOException("Can't make directory " + dir + " - it is already a file."); | ||||||
|             SanitizedFile file = new SanitizedFile(directory, fileName); |             } | ||||||
|             file.delete(); |  | ||||||
|             Utils.symlinkOrCopyFile(new SanitizedFile(new File(symlinkPrefix), fileName), file); |  | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         if (!dir.mkdir()) { | ||||||
|  |             throw new IOException("An error occurred trying to create directory " + dir); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void attemptToDelete(@NonNull File file) { | ||||||
|  |         if (!file.delete()) { | ||||||
|  |             Log.e(TAG, "Could not delete \"" + file.getAbsolutePath() + "\"."); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void symlinkEntireWebRootElsewhere(String symlinkPrefix, File directory) { | ||||||
|  |         symlinkFileElsewhere("index.html", symlinkPrefix, directory); | ||||||
|  |         for(String fileName : WEB_ROOT_ASSET_FILES) { | ||||||
|  |             symlinkFileElsewhere(fileName, symlinkPrefix, directory); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void symlinkFileElsewhere(String fileName, String symlinkPrefix, File directory) { | ||||||
|  |         SanitizedFile index = new SanitizedFile(directory, fileName); | ||||||
|  |         attemptToDelete(index); | ||||||
|  |         Utils.symlinkOrCopyFile(new SanitizedFile(new File(directory, symlinkPrefix), fileName), index); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void deleteContents(File path) { |     private void deleteContents(File path) { | ||||||
| @ -208,7 +222,7 @@ public class LocalRepoManager { | |||||||
|                 if (file.isDirectory()) { |                 if (file.isDirectory()) { | ||||||
|                     deleteContents(file); |                     deleteContents(file); | ||||||
|                 } else { |                 } else { | ||||||
|                     file.delete(); |                     attemptToDelete(file); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @ -219,7 +233,7 @@ public class LocalRepoManager { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void copyApksToRepo() { |     public void copyApksToRepo() { | ||||||
|         copyApksToRepo(new ArrayList<String>(apps.keySet())); |         copyApksToRepo(new ArrayList<>(apps.keySet())); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void copyApksToRepo(List<String> appsToCopy) { |     public void copyApksToRepo(List<String> appsToCopy) { | ||||||
| @ -236,11 +250,6 @@ public class LocalRepoManager { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public interface ScanListener { |  | ||||||
|         public void processedApp(String packageName, int index, int total); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @TargetApi(9) |  | ||||||
|     public void addApp(Context context, String packageName) { |     public void addApp(Context context, String packageName) { | ||||||
|         App app; |         App app; | ||||||
|         try { |         try { | ||||||
| @ -249,15 +258,7 @@ public class LocalRepoManager { | |||||||
|                 return; |                 return; | ||||||
|             PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_META_DATA); |             PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_META_DATA); | ||||||
|             app.icon = getIconFile(packageName, packageInfo.versionCode).getName(); |             app.icon = getIconFile(packageName, packageInfo.versionCode).getName(); | ||||||
|         } catch (NameNotFoundException e) { |         } catch (NameNotFoundException | CertificateEncodingException | IOException e) { | ||||||
|             Log.e(TAG, "Error adding app to local repo: " + e.getMessage()); |  | ||||||
|             Log.e(TAG, Log.getStackTraceString(e)); |  | ||||||
|             return; |  | ||||||
|         } catch (CertificateEncodingException e) { |  | ||||||
|             Log.e(TAG, "Error adding app to local repo: " + e.getMessage()); |  | ||||||
|             Log.e(TAG, Log.getStackTraceString(e)); |  | ||||||
|             return; |  | ||||||
|         } catch (IOException e) { |  | ||||||
|             Log.e(TAG, "Error adding app to local repo: " + e.getMessage()); |             Log.e(TAG, "Error adding app to local repo: " + e.getMessage()); | ||||||
|             Log.e(TAG, Log.getStackTraceString(e)); |             Log.e(TAG, Log.getStackTraceString(e)); | ||||||
|             return; |             return; | ||||||
| @ -266,12 +267,8 @@ public class LocalRepoManager { | |||||||
|         apps.put(packageName, app); |         apps.put(packageName, app); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void removeApp(String packageName) { |  | ||||||
|         apps.remove(packageName); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public List<String> getApps() { |     public List<String> getApps() { | ||||||
|         return new ArrayList<String>(apps.keySet()); |         return new ArrayList<>(apps.keySet()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void copyIconsToRepo() { |     public void copyIconsToRepo() { | ||||||
| @ -290,8 +287,6 @@ public class LocalRepoManager { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Extracts the icon from an APK and writes it to the repo as a PNG |      * Extracts the icon from an APK and writes it to the repo as a PNG | ||||||
|      * |  | ||||||
|      * @return path to the PNG file |  | ||||||
|      */ |      */ | ||||||
|     public void copyIconToRepo(Drawable drawable, String packageName, int versionCode) { |     public void copyIconToRepo(Drawable drawable, String packageName, int versionCode) { | ||||||
|         Bitmap bitmap; |         Bitmap bitmap; | ||||||
| @ -319,141 +314,139 @@ public class LocalRepoManager { | |||||||
|         return new File(iconsDir, packageName + "_" + versionCode + ".png"); |         return new File(iconsDir, packageName + "_" + versionCode + ".png"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // TODO this needs to be ported to < android-8 |     /** | ||||||
|     @TargetApi(8) |      * Helper class to aid in constructing index.xml file. | ||||||
|     private void writeIndexXML() throws TransformerException, ParserConfigurationException, LocalRepoKeyStore.InitException { |      * It uses the PullParser API, because the DOM api is only able to be serialized from | ||||||
|         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |      * API 8 upwards, but we support 7 at time of implementation. | ||||||
|         DocumentBuilder builder = factory.newDocumentBuilder(); |      */ | ||||||
|  |     public static class IndexXmlBuilder { | ||||||
| 
 | 
 | ||||||
|         Document doc = builder.newDocument(); |         @NonNull | ||||||
|         Element rootElement = doc.createElement("fdroid"); |         private final XmlSerializer serializer; | ||||||
|         doc.appendChild(rootElement); |  | ||||||
| 
 | 
 | ||||||
|         // max age is an EditTextPreference, which is always a String |         @NonNull | ||||||
|         int repoMaxAge = Float.valueOf(prefs.getString("max_repo_age_days", |         private final Map<String, App> apps; | ||||||
|                 DEFAULT_REPO_MAX_AGE_DAYS)).intValue(); |  | ||||||
| 
 | 
 | ||||||
|         String repoName = Preferences.get().getLocalRepoName(); |         @NonNull | ||||||
|  |         private final Context context; | ||||||
| 
 | 
 | ||||||
|         Element repo = doc.createElement("repo"); |         @NonNull | ||||||
|         repo.setAttribute("icon", "blah.png"); |         private final DateFormat dateToStr = new SimpleDateFormat("yyyy-MM-dd", Locale.US); | ||||||
|         repo.setAttribute("maxage", String.valueOf(repoMaxAge)); |  | ||||||
|         repo.setAttribute("name", repoName + " on " + ipAddressString); |  | ||||||
|         repo.setAttribute("pubkey", Hasher.hex(LocalRepoKeyStore.get(context).getCertificate())); |  | ||||||
|         long timestamp = System.currentTimeMillis() / 1000L; |  | ||||||
|         repo.setAttribute("timestamp", String.valueOf(timestamp)); |  | ||||||
|         rootElement.appendChild(repo); |  | ||||||
| 
 | 
 | ||||||
|         Element repoDesc = doc.createElement("description"); |         public IndexXmlBuilder(@NonNull Context context, @NonNull Map<String, App> apps) throws XmlPullParserException, IOException { | ||||||
|         repoDesc.setTextContent("A local FDroid repo generated from apps installed on " + repoName); |             this.context = context; | ||||||
|         repo.appendChild(repoDesc); |             this.apps = apps; | ||||||
|  |             serializer = XmlPullParserFactory.newInstance().newSerializer(); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         SimpleDateFormat dateToStr = new SimpleDateFormat("yyyy-MM-dd", Locale.US); |         public void build(Writer output) throws IOException, LocalRepoKeyStore.InitException { | ||||||
|         for (Entry<String, App> entry : apps.entrySet()) { |             serializer.setOutput(output); | ||||||
|             App app = entry.getValue(); |             serializer.startDocument(null, null); | ||||||
|             Element application = doc.createElement("application"); |             tagFdroid(); | ||||||
|             application.setAttribute("id", app.id); |             serializer.endDocument(); | ||||||
|             rootElement.appendChild(application); |         } | ||||||
| 
 | 
 | ||||||
|             Element appID = doc.createElement("id"); |         private void tagFdroid() throws IOException, LocalRepoKeyStore.InitException { | ||||||
|             appID.setTextContent(app.id); |             serializer.startTag("", "fdroid"); | ||||||
|             application.appendChild(appID); |             tagRepo(); | ||||||
|  |             for (Entry<String, App> entry : apps.entrySet()) { | ||||||
|  |                 tagApplication(entry.getValue()); | ||||||
|  |             } | ||||||
|  |             serializer.endTag("", "fdroid"); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|             Element added = doc.createElement("added"); |         private void tagRepo() throws IOException, LocalRepoKeyStore.InitException { | ||||||
|             added.setTextContent(dateToStr.format(app.added)); |  | ||||||
|             application.appendChild(added); |  | ||||||
| 
 | 
 | ||||||
|             Element lastUpdated = doc.createElement("lastupdated"); |             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|             lastUpdated.setTextContent(dateToStr.format(app.lastUpdated)); |  | ||||||
|             application.appendChild(lastUpdated); |  | ||||||
| 
 | 
 | ||||||
|             Element name = doc.createElement("name"); |             // max age is an EditTextPreference, which is always a String | ||||||
|             name.setTextContent(app.name); |             int repoMaxAge = Float.valueOf(prefs.getString("max_repo_age_days", DEFAULT_REPO_MAX_AGE_DAYS)).intValue(); | ||||||
|             application.appendChild(name); |  | ||||||
| 
 | 
 | ||||||
|             Element summary = doc.createElement("summary"); |             serializer.startTag("", "repo"); | ||||||
|             summary.setTextContent(app.summary); |  | ||||||
|             application.appendChild(summary); |  | ||||||
| 
 | 
 | ||||||
|             Element desc = doc.createElement("desc"); |             serializer.attribute("", "icon", "blah.png"); | ||||||
|             desc.setTextContent(app.description); |             serializer.attribute("", "maxage", String.valueOf(repoMaxAge)); | ||||||
|             application.appendChild(desc); |             serializer.attribute("", "name", Preferences.get().getLocalRepoName() + " on " + FDroidApp.ipAddressString); | ||||||
|  |             serializer.attribute("", "pubkey", Hasher.hex(LocalRepoKeyStore.get(context).getCertificate())); | ||||||
|  |             long timestamp = System.currentTimeMillis() / 1000L; | ||||||
|  |             serializer.attribute("", "timestamp", String.valueOf(timestamp)); | ||||||
| 
 | 
 | ||||||
|             Element icon = doc.createElement("icon"); |             tag("description", "A local FDroid repo generated from apps installed on " + Preferences.get().getLocalRepoName()); | ||||||
|             icon.setTextContent(app.icon); |  | ||||||
|             application.appendChild(icon); |  | ||||||
| 
 | 
 | ||||||
|             Element license = doc.createElement("license"); |             serializer.endTag("", "repo"); | ||||||
|             license.setTextContent("Unknown"); |  | ||||||
|             application.appendChild(license); |  | ||||||
| 
 | 
 | ||||||
|             Element categories = doc.createElement("categories"); |         } | ||||||
|             categories.setTextContent("LocalRepo," + repoName); |  | ||||||
|             application.appendChild(categories); |  | ||||||
| 
 | 
 | ||||||
|             Element category = doc.createElement("category"); |         /** | ||||||
|             category.setTextContent("LocalRepo," + repoName); |          * Helper function to start a tag called "name", fill it with text "text", and then | ||||||
|             application.appendChild(category); |          * end the tag in a more concise manner. | ||||||
|  |          */ | ||||||
|  |         private void tag(String name, String text) throws IOException { | ||||||
|  |             serializer.startTag("", name).text(text).endTag("", name); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|             Element web = doc.createElement("web"); |         /** | ||||||
|             application.appendChild(web); |          * Alias for {@link org.fdroid.fdroid.localrepo.LocalRepoManager.IndexXmlBuilder#tag(String, String)} | ||||||
|  |          * That accepts a number instead of string. | ||||||
|  |          * @see IndexXmlBuilder#tag(String, String) | ||||||
|  |          */ | ||||||
|  |         private void tag(String name, long number) throws IOException { | ||||||
|  |             tag(name, String.valueOf(number)); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|             Element source = doc.createElement("source"); |         /** | ||||||
|             application.appendChild(source); |          * Alias for {@link org.fdroid.fdroid.localrepo.LocalRepoManager.IndexXmlBuilder#tag(String, String)} | ||||||
|  |          * that accepts a date instead of a string. | ||||||
|  |          * @see IndexXmlBuilder#tag(String, String) | ||||||
|  |          */ | ||||||
|  |         private void tag(String name, Date date) throws IOException { | ||||||
|  |             tag(name, dateToStr.format(date)); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|             Element tracker = doc.createElement("tracker"); |         private void tagApplication(App app) throws IOException { | ||||||
|             application.appendChild(tracker); |             serializer.startTag("", "application"); | ||||||
|  |             serializer.attribute("", "id", app.id); | ||||||
| 
 | 
 | ||||||
|             Element marketVersion = doc.createElement("marketversion"); |             tag("id", app.id); | ||||||
|             marketVersion.setTextContent(app.installedApk.version); |             tag("added", app.added); | ||||||
|             application.appendChild(marketVersion); |             tag("lastupdated", app.lastUpdated); | ||||||
|  |             tag("name", app.name); | ||||||
|  |             tag("summary", app.summary); | ||||||
|  |             tag("icon", app.icon); | ||||||
|  |             tag("desc", app.description); | ||||||
|  |             tag("license", "Unknown"); | ||||||
|  |             tag("categories", "LocalRepo," + Preferences.get().getLocalRepoName()); | ||||||
|  |             tag("category", "LocalRepo," + Preferences.get().getLocalRepoName()); | ||||||
|  |             tag("web", "web"); | ||||||
|  |             tag("source", "source"); | ||||||
|  |             tag("tracker", "tracker"); | ||||||
|  |             tag("marketversion", app.installedApk.version); | ||||||
|  |             tag("marketvercode", app.installedApk.vercode); | ||||||
| 
 | 
 | ||||||
|             Element marketVerCode = doc.createElement("marketvercode"); |             tagPackage(app); | ||||||
|             marketVerCode.setTextContent(String.valueOf(app.installedApk.vercode)); |  | ||||||
|             application.appendChild(marketVerCode); |  | ||||||
| 
 | 
 | ||||||
|             Element packageNode = doc.createElement("package"); |             serializer.endTag("", "application"); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|             Element version = doc.createElement("version"); |         private void tagPackage(App app) throws IOException { | ||||||
|             version.setTextContent(app.installedApk.version); |             serializer.startTag("", "package"); | ||||||
|             packageNode.appendChild(version); |  | ||||||
| 
 | 
 | ||||||
|             // F-Droid unfortunately calls versionCode versioncode... |             tag("version", app.installedApk.version); | ||||||
|             Element versioncode = doc.createElement("versioncode"); |             tag("versioncode", app.installedApk.vercode); | ||||||
|             versioncode.setTextContent(String.valueOf(app.installedApk.vercode)); |             tag("apkname", app.installedApk.apkName); | ||||||
|             packageNode.appendChild(versioncode); |             tagHash(app); | ||||||
|  |             tag("sig", app.installedApk.sig.toLowerCase(Locale.US)); | ||||||
|  |             tag("size", app.installedApk.installedFile.length()); | ||||||
|  |             tag("sdkver", app.installedApk.minSdkVersion); | ||||||
|  |             tag("added", app.installedApk.added); | ||||||
|  |             tagFeatures(app); | ||||||
|  |             tagPermissions(app); | ||||||
| 
 | 
 | ||||||
|             Element apkname = doc.createElement("apkname"); |             serializer.endTag("", "package"); | ||||||
|             apkname.setTextContent(app.installedApk.apkName); |         } | ||||||
|             packageNode.appendChild(apkname); |  | ||||||
| 
 | 
 | ||||||
|             Element hash = doc.createElement("hash"); |         private void tagPermissions(App app) throws IOException { | ||||||
|             hash.setAttribute("type", app.installedApk.hashType); |             serializer.startTag("", "permissions"); | ||||||
|             hash.setTextContent(app.installedApk.hash.toLowerCase(Locale.US)); |  | ||||||
|             packageNode.appendChild(hash); |  | ||||||
| 
 |  | ||||||
|             Element sig = doc.createElement("sig"); |  | ||||||
|             sig.setTextContent(app.installedApk.sig.toLowerCase(Locale.US)); |  | ||||||
|             packageNode.appendChild(sig); |  | ||||||
| 
 |  | ||||||
|             Element size = doc.createElement("size"); |  | ||||||
|             size.setTextContent(String.valueOf(app.installedApk.installedFile.length())); |  | ||||||
|             packageNode.appendChild(size); |  | ||||||
| 
 |  | ||||||
|             Element sdkver = doc.createElement("sdkver"); |  | ||||||
|             sdkver.setTextContent(String.valueOf(app.installedApk.minSdkVersion)); |  | ||||||
|             packageNode.appendChild(sdkver); |  | ||||||
| 
 |  | ||||||
|             Element apkAdded = doc.createElement("added"); |  | ||||||
|             apkAdded.setTextContent(dateToStr.format(app.installedApk.added)); |  | ||||||
|             packageNode.appendChild(apkAdded); |  | ||||||
| 
 |  | ||||||
|             Element features = doc.createElement("features"); |  | ||||||
|             if (app.installedApk.features != null) |  | ||||||
|                 features.setTextContent(Utils.CommaSeparatedList.str(app.installedApk.features)); |  | ||||||
|             packageNode.appendChild(features); |  | ||||||
| 
 |  | ||||||
|             Element permissions = doc.createElement("permissions"); |  | ||||||
|             if (app.installedApk.permissions != null) { |             if (app.installedApk.permissions != null) { | ||||||
|                 StringBuilder buff = new StringBuilder(); |                 StringBuilder buff = new StringBuilder(); | ||||||
| 
 | 
 | ||||||
| @ -463,28 +456,32 @@ public class LocalRepoManager { | |||||||
|                 } |                 } | ||||||
|                 String out = buff.toString(); |                 String out = buff.toString(); | ||||||
|                 if (!TextUtils.isEmpty(out)) |                 if (!TextUtils.isEmpty(out)) | ||||||
|                     permissions.setTextContent(out.substring(0, out.length() - 1)); |                     serializer.text(out.substring(0, out.length() - 1)); | ||||||
|             } |             } | ||||||
|             packageNode.appendChild(permissions); |             serializer.endTag("", "permissions"); | ||||||
| 
 |  | ||||||
|             application.appendChild(packageNode); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         TransformerFactory transformerFactory = TransformerFactory.newInstance(); |         private void tagFeatures(App app) throws IOException { | ||||||
|         Transformer transformer = transformerFactory.newTransformer(); |             serializer.startTag("", "features"); | ||||||
|  |             if (app.installedApk.features != null) | ||||||
|  |                 serializer.text(Utils.CommaSeparatedList.str(app.installedApk.features)); | ||||||
|  |             serializer.endTag("", "features"); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         DOMSource domSource = new DOMSource(doc); |         private void tagHash(App app) throws IOException { | ||||||
|         StreamResult result = new StreamResult(xmlIndex); |             serializer.startTag("", "hash"); | ||||||
| 
 |             serializer.attribute("", "type", app.installedApk.hashType); | ||||||
|         transformer.transform(domSource, result); |             serializer.text(app.installedApk.hash.toLowerCase(Locale.US)); | ||||||
|  |             serializer.endTag("", "hash"); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void writeIndexJar() throws IOException { |     public void writeIndexJar() throws IOException { | ||||||
|         try { |         try { | ||||||
|             writeIndexXML(); |             new IndexXmlBuilder(context, apps).build(new FileWriter(xmlIndex)); | ||||||
|         } catch (Exception e) { |         } catch (Exception e) { | ||||||
|             Toast.makeText(context, R.string.failed_to_create_index, Toast.LENGTH_LONG).show(); |  | ||||||
|             Log.e(TAG, Log.getStackTraceString(e)); |             Log.e(TAG, Log.getStackTraceString(e)); | ||||||
|  |             Toast.makeText(context, R.string.failed_to_create_index, Toast.LENGTH_LONG).show(); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -512,8 +509,9 @@ public class LocalRepoManager { | |||||||
|         } catch (LocalRepoKeyStore.InitException e) { |         } catch (LocalRepoKeyStore.InitException e) { | ||||||
|             throw new IOException("Could not sign index - keystore failed to initialize"); |             throw new IOException("Could not sign index - keystore failed to initialize"); | ||||||
|         } finally { |         } finally { | ||||||
|             xmlIndexJarUnsigned.delete(); |             attemptToDelete(xmlIndexJarUnsigned); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -82,8 +82,7 @@ public class WifiStateChangeService extends Service { | |||||||
| 
 | 
 | ||||||
|                 Context context = WifiStateChangeService.this.getApplicationContext(); |                 Context context = WifiStateChangeService.this.getApplicationContext(); | ||||||
|                 LocalRepoManager lrm = LocalRepoManager.get(context); |                 LocalRepoManager lrm = LocalRepoManager.get(context); | ||||||
|                 lrm.setUriString(FDroidApp.repo.address); |                 lrm.writeIndexPage(Utils.getSharingUri(FDroidApp.repo).toString()); | ||||||
|                 lrm.writeIndexPage(Utils.getSharingUri(context, FDroidApp.repo).toString()); |  | ||||||
| 
 | 
 | ||||||
|                 if (isCancelled()) |                 if (isCancelled()) | ||||||
|                     return null; |                     return null; | ||||||
|  | |||||||
| @ -246,7 +246,7 @@ public class LocalRepoActivity extends ActionBarActivity { | |||||||
|          * wifi AP to join. Lots of QR Scanners are buggy and do not respect |          * wifi AP to join. Lots of QR Scanners are buggy and do not respect | ||||||
|          * custom URI schemes, so we have to use http:// or https:// :-( |          * custom URI schemes, so we have to use http:// or https:// :-( | ||||||
|          */ |          */ | ||||||
|         final String qrUriString = Utils.getSharingUri(this, FDroidApp.repo).toString() |         final String qrUriString = Utils.getSharingUri(FDroidApp.repo).toString() | ||||||
|                 .replaceFirst("fdroidrepo", "http") |                 .replaceFirst("fdroidrepo", "http") | ||||||
|                 .replaceAll("ssid=[^?]*", "") |                 .replaceAll("ssid=[^?]*", "") | ||||||
|                 .toUpperCase(Locale.ENGLISH); |                 .toUpperCase(Locale.ENGLISH); | ||||||
| @ -270,7 +270,7 @@ public class LocalRepoActivity extends ActionBarActivity { | |||||||
|             if (nfcAdapter == null) |             if (nfcAdapter == null) | ||||||
|                 return; |                 return; | ||||||
|             nfcAdapter.setNdefPushMessage(new NdefMessage(new NdefRecord[] { |             nfcAdapter.setNdefPushMessage(new NdefMessage(new NdefRecord[] { | ||||||
|                     NdefRecord.createUri(Utils.getSharingUri(this, FDroidApp.repo)), |                     NdefRecord.createUri(Utils.getSharingUri(FDroidApp.repo)), | ||||||
|             }), this); |             }), this); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -292,7 +292,7 @@ public class LocalRepoActivity extends ActionBarActivity { | |||||||
|             progressDialog = new ProgressDialog(c); |             progressDialog = new ProgressDialog(c); | ||||||
|             progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); |             progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); | ||||||
|             progressDialog.setTitle(R.string.updating); |             progressDialog.setTitle(R.string.updating); | ||||||
|             sharingUri = Utils.getSharingUri(c, FDroidApp.repo); |             sharingUri = Utils.getSharingUri(FDroidApp.repo); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         @Override |         @Override | ||||||
|  | |||||||
| @ -611,7 +611,7 @@ public class ManageReposActivity extends ActionBarActivity { | |||||||
| 
 | 
 | ||||||
|         @Override |         @Override | ||||||
|         public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { |         public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { | ||||||
|             Uri uri = RepoProvider.getContentUri(); |             Uri uri = RepoProvider.allExceptSwapUri(); | ||||||
|             Log.i(TAG, "Creating repo loader '" + uri + "'."); |             Log.i(TAG, "Creating repo loader '" + uri + "'."); | ||||||
|             String[] projection = { |             String[] projection = { | ||||||
|                     RepoProvider.DataColumns._ID, |                     RepoProvider.DataColumns._ID, | ||||||
|  | |||||||
| @ -66,7 +66,7 @@ public class RepoDetailsActivity extends ActionBarActivity { | |||||||
| 
 | 
 | ||||||
|     @TargetApi(14) |     @TargetApi(14) | ||||||
|     private void setNfc() { |     private void setNfc() { | ||||||
|         if (NfcHelper.setPushMessage(this, Utils.getSharingUri(this, repo))) { |         if (NfcHelper.setPushMessage(this, Utils.getSharingUri(repo))) { | ||||||
|             findViewById(android.R.id.content).post(new Runnable() { |             findViewById(android.R.id.content).post(new Runnable() { | ||||||
|                 @Override |                 @Override | ||||||
|                 public void run() { |                 public void run() { | ||||||
|  | |||||||
| @ -142,11 +142,19 @@ abstract public class AppListFragment extends ThemeableListFragment implements | |||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |     public void onItemClick(AdapterView<?> parent, View view, int position, long id) { | ||||||
|         final App app = new App((Cursor)getListView().getItemAtPosition(position)); |         // Cursor is null in the swap list when touching the first item. | ||||||
|         Intent intent = new Intent(getActivity(), AppDetails.class); |         Cursor cursor = (Cursor)getListView().getItemAtPosition(position); | ||||||
|         intent.putExtra(AppDetails.EXTRA_APPID, app.id); |         if (cursor != null) { | ||||||
|         intent.putExtra(AppDetails.EXTRA_FROM, getFromTitle()); |             final App app = new App(cursor); | ||||||
|         startActivityForResult(intent, FDroid.REQUEST_APPDETAILS); |             Intent intent = getAppDetailsIntent(); | ||||||
|  |             intent.putExtra(AppDetails.EXTRA_APPID, app.id); | ||||||
|  |             intent.putExtra(AppDetails.EXTRA_FROM, getFromTitle()); | ||||||
|  |             startActivityForResult(intent, FDroid.REQUEST_APPDETAILS); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected Intent getAppDetailsIntent() { | ||||||
|  |         return new Intent(getActivity(), AppDetails.class); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | |||||||
| @ -4,7 +4,10 @@ import android.app.Activity; | |||||||
| import android.content.ContentValues; | import android.content.ContentValues; | ||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
| import android.support.v4.app.Fragment; | import android.support.v4.app.Fragment; | ||||||
|  | import android.util.Log; | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| @ -18,8 +21,13 @@ import org.fdroid.fdroid.data.RepoProvider; | |||||||
| 
 | 
 | ||||||
| public class ConfirmReceiveSwapFragment extends Fragment implements ProgressListener { | public class ConfirmReceiveSwapFragment extends Fragment implements ProgressListener { | ||||||
| 
 | 
 | ||||||
|  |     private static final String TAG = "org.fdroid.fdroid.views.swap.ConfirmReceiveSwapFragment"; | ||||||
|  | 
 | ||||||
|     private NewRepoConfig newRepoConfig; |     private NewRepoConfig newRepoConfig; | ||||||
| 
 | 
 | ||||||
|  |     @Nullable | ||||||
|  |     private Repo repo; | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||||
| 
 | 
 | ||||||
| @ -60,26 +68,33 @@ public class ConfirmReceiveSwapFragment extends Fragment implements ProgressList | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void confirm() { |     private void confirm() { | ||||||
|         Repo repo = ensureRepoExists(); |         this.repo = ensureRepoExists(); | ||||||
|         UpdateService.updateRepoNow(repo.address, getActivity()).setListener(this); |         UpdateService.updateRepoNow(this.repo.address, getActivity()).setListener(this); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @NonNull | ||||||
|     private Repo ensureRepoExists() { |     private Repo ensureRepoExists() { | ||||||
|         // TODO: newRepoConfig.getUri() will include a fingerprint, which may not match with |         // TODO: newRepoConfig.getUri() will include a fingerprint, which may not match with | ||||||
|         // the repos address in the database. |         // the repos address in the database. | ||||||
|         Repo repo = RepoProvider.Helper.findByAddress(getActivity(), newRepoConfig.getUriString()); |         Repo repo = RepoProvider.Helper.findByAddress(getActivity(), newRepoConfig.getUriString()); | ||||||
|         if (repo == null) { |         if (repo == null) { | ||||||
|             ContentValues values = new ContentValues(5); |             ContentValues values = new ContentValues(6); | ||||||
| 
 | 
 | ||||||
|              // TODO: i18n and think about most appropriate name. Although ideally, it will not be seen often, |              // TODO: i18n and think about most appropriate name. Although it wont be visible in | ||||||
|              // because we're whacking a pretty UI over the swap process so they don't need to "Manage repos"... |              // the "Manage repos" UI after being marked as a swap repo here... | ||||||
|             values.put(RepoProvider.DataColumns.NAME, "Swap"); |             values.put(RepoProvider.DataColumns.NAME, "Swap"); | ||||||
|             values.put(RepoProvider.DataColumns.ADDRESS, newRepoConfig.getUriString()); |             values.put(RepoProvider.DataColumns.ADDRESS, newRepoConfig.getUriString()); | ||||||
|             values.put(RepoProvider.DataColumns.DESCRIPTION, ""); // TODO; |             values.put(RepoProvider.DataColumns.DESCRIPTION, ""); // TODO; | ||||||
|             values.put(RepoProvider.DataColumns.FINGERPRINT, newRepoConfig.getFingerprint()); |             values.put(RepoProvider.DataColumns.FINGERPRINT, newRepoConfig.getFingerprint()); | ||||||
|             values.put(RepoProvider.DataColumns.IN_USE, true); |             values.put(RepoProvider.DataColumns.IN_USE, true); | ||||||
|  |             values.put(RepoProvider.DataColumns.IS_SWAP, true); | ||||||
|             Uri uri = RepoProvider.Helper.insert(getActivity(), values); |             Uri uri = RepoProvider.Helper.insert(getActivity(), values); | ||||||
|             repo = RepoProvider.Helper.findByUri(getActivity(), uri); |             repo = RepoProvider.Helper.findByUri(getActivity(), uri); | ||||||
|  |         } else if (!repo.isSwap) { | ||||||
|  |             Log.d(TAG, "Old local repo being marked as \"Swap\" repo, so that it wont appear in the list of repositories in the future."); | ||||||
|  |             ContentValues values = new ContentValues(1); | ||||||
|  |             values.put(RepoProvider.DataColumns.IS_SWAP, true); | ||||||
|  |             RepoProvider.Helper.update(getActivity(), repo, values); | ||||||
|         } |         } | ||||||
|         return repo; |         return repo; | ||||||
|     } |     } | ||||||
| @ -93,7 +108,7 @@ public class ConfirmReceiveSwapFragment extends Fragment implements ProgressList | |||||||
| 
 | 
 | ||||||
|         if (event.type.equals(UpdateService.EVENT_COMPLETE_AND_SAME) || |         if (event.type.equals(UpdateService.EVENT_COMPLETE_AND_SAME) || | ||||||
|                 event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) { |                 event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) { | ||||||
|             ((ConnectSwapActivity)getActivity()).onRepoUpdated(); |             ((ConnectSwapActivity)getActivity()).onRepoUpdated(repo); | ||||||
|             /*Intent intent = new Intent(); |             /*Intent intent = new Intent(); | ||||||
|             intent.putExtra("category", newRepoConfig.getHost()); // TODO: Load repo from database to get proper name. This is what the category we want to select will be called. |             intent.putExtra("category", newRepoConfig.getHost()); // TODO: Load repo from database to get proper name. This is what the category we want to select will be called. | ||||||
|             getActivity().setResult(Activity.RESULT_OK, intent); |             getActivity().setResult(Activity.RESULT_OK, intent); | ||||||
|  | |||||||
| @ -5,10 +5,11 @@ import android.os.Bundle; | |||||||
| import android.support.v4.app.FragmentActivity; | import android.support.v4.app.FragmentActivity; | ||||||
| import android.support.v4.app.FragmentManager; | import android.support.v4.app.FragmentManager; | ||||||
| 
 | 
 | ||||||
|  | import org.fdroid.fdroid.data.Repo; | ||||||
|  | 
 | ||||||
| public class ConnectSwapActivity extends FragmentActivity { | public class ConnectSwapActivity extends FragmentActivity { | ||||||
| 
 | 
 | ||||||
|     private static final String STATE_CONFIRM = "startSwap"; |     private static final String STATE_CONFIRM = "startSwap"; | ||||||
|     private static final String STATE_APP_LIST = "swapAppList"; |  | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onCreate(Bundle savedInstanceState) { |     public void onCreate(Bundle savedInstanceState) { | ||||||
| @ -42,10 +43,9 @@ public class ConnectSwapActivity extends FragmentActivity { | |||||||
|         return lastFragment.getName(); |         return lastFragment.getName(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void onRepoUpdated() { |     public void onRepoUpdated(Repo repo) { | ||||||
| 
 |  | ||||||
|         Intent intent = new Intent(this, SwapAppListActivity.class); |         Intent intent = new Intent(this, SwapAppListActivity.class); | ||||||
|  |         intent.putExtra(SwapAppListActivity.EXTRA_REPO_ID, repo.getId()); | ||||||
|         startActivity(intent); |         startActivity(intent); | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,27 +1,22 @@ | |||||||
| package org.fdroid.fdroid.views.swap; | package org.fdroid.fdroid.views.swap; | ||||||
| 
 | 
 | ||||||
|  | import android.content.Context; | ||||||
| import android.content.pm.PackageManager; | import android.content.pm.PackageManager; | ||||||
| import android.database.Cursor; | import android.database.Cursor; | ||||||
| import android.graphics.drawable.Drawable; | import android.graphics.drawable.Drawable; | ||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
| import android.support.v4.app.LoaderManager; | import android.support.v4.app.LoaderManager; | ||||||
| import android.support.v4.content.CursorLoader; | import android.support.v4.content.CursorLoader; | ||||||
| import android.support.v4.content.Loader; | import android.support.v4.content.Loader; | ||||||
| import android.support.v4.view.MenuItemCompat; | import android.support.v4.view.MenuItemCompat; | ||||||
| import android.support.v4.widget.SimpleCursorAdapter; | import android.support.v4.widget.CursorAdapter; | ||||||
|  | import android.support.v7.widget.SearchView; | ||||||
| import android.text.TextUtils; | import android.text.TextUtils; | ||||||
| import android.view.ActionMode; | import android.view.*; | ||||||
| import android.view.ContextThemeWrapper; | import android.widget.*; | ||||||
| import android.view.Menu; |  | ||||||
| import android.view.MenuInflater; |  | ||||||
| import android.view.MenuItem; |  | ||||||
| import android.view.View; |  | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.LinearLayout; |  | ||||||
| import android.widget.ListView; |  | ||||||
| import android.widget.SearchView; |  | ||||||
| import android.widget.TextView; |  | ||||||
| import org.fdroid.fdroid.FDroidApp; | import org.fdroid.fdroid.FDroidApp; | ||||||
| import org.fdroid.fdroid.R; | import org.fdroid.fdroid.R; | ||||||
| import org.fdroid.fdroid.data.InstalledAppProvider; | import org.fdroid.fdroid.data.InstalledAppProvider; | ||||||
| @ -34,11 +29,13 @@ import java.util.Set; | |||||||
| public class SelectAppsFragment extends ThemeableListFragment | public class SelectAppsFragment extends ThemeableListFragment | ||||||
|     implements LoaderManager.LoaderCallbacks<Cursor>, SearchView.OnQueryTextListener { |     implements LoaderManager.LoaderCallbacks<Cursor>, SearchView.OnQueryTextListener { | ||||||
| 
 | 
 | ||||||
|     private PackageManager packageManager; |     @SuppressWarnings("UnusedDeclaration") | ||||||
|     private Drawable defaultAppIcon; |     private static final String TAG = "org.fdroid.fdroid.views.swap.SelectAppsFragment"; | ||||||
|     private ActionMode mActionMode = null; | 
 | ||||||
|     private String mCurrentFilterString; |     private String mCurrentFilterString; | ||||||
|     private Set<String> previouslySelectedApps = new HashSet<String>(); | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private final Set<String> previouslySelectedApps = new HashSet<>(); | ||||||
| 
 | 
 | ||||||
|     public Set<String> getSelectedApps() { |     public Set<String> getSelectedApps() { | ||||||
|         return FDroidApp.selectedApps; |         return FDroidApp.selectedApps; | ||||||
| @ -52,10 +49,18 @@ public class SelectAppsFragment extends ThemeableListFragment | |||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { |     public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { | ||||||
|         menuInflater.inflate(R.menu.swap_next, menu); |         menuInflater.inflate(R.menu.swap_next_search, menu); | ||||||
|         MenuItem nextMenuItem = menu.findItem(R.id.action_next); |         MenuItem nextMenuItem = menu.findItem(R.id.action_next); | ||||||
|         int flags = MenuItemCompat.SHOW_AS_ACTION_ALWAYS | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT; |         int flags = MenuItemCompat.SHOW_AS_ACTION_ALWAYS | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT; | ||||||
|         MenuItemCompat.setShowAsAction(nextMenuItem, flags); |         MenuItemCompat.setShowAsAction(nextMenuItem, flags); | ||||||
|  | 
 | ||||||
|  |         SearchView searchView = new SearchView(getActivity()); | ||||||
|  | 
 | ||||||
|  |         MenuItem searchMenuItem = menu.findItem(R.id.action_search); | ||||||
|  |         MenuItemCompat.setActionView(searchMenuItem, searchView); | ||||||
|  |         MenuItemCompat.setShowAsAction(searchMenuItem, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); | ||||||
|  | 
 | ||||||
|  |         searchView.setOnQueryTextListener(this); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
| @ -96,46 +101,10 @@ public class SelectAppsFragment extends ThemeableListFragment | |||||||
| 
 | 
 | ||||||
|         setEmptyText(getString(R.string.no_applications_found)); |         setEmptyText(getString(R.string.no_applications_found)); | ||||||
| 
 | 
 | ||||||
|         packageManager = getActivity().getPackageManager(); |  | ||||||
|         defaultAppIcon = getResources().getDrawable(android.R.drawable.sym_def_app_icon); |  | ||||||
| 
 |  | ||||||
|         ListView listView = getListView(); |         ListView listView = getListView(); | ||||||
|         listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); |         listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); | ||||||
|         SimpleCursorAdapter adapter = new SimpleCursorAdapter( |  | ||||||
|                 new ContextThemeWrapper(getActivity(), R.style.SwapTheme_AppList_ListItem), |  | ||||||
|                 R.layout.select_local_apps_list_item, |  | ||||||
|                 null, |  | ||||||
|                 new String[] { |  | ||||||
|                         InstalledAppProvider.DataColumns.APPLICATION_LABEL, |  | ||||||
|                         InstalledAppProvider.DataColumns.APP_ID, |  | ||||||
|                 }, |  | ||||||
|                 new int[] { |  | ||||||
|                         R.id.application_label, |  | ||||||
|                         R.id.package_name, |  | ||||||
|                 }); |  | ||||||
|         adapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() { |  | ||||||
| 
 | 
 | ||||||
|             @Override |         setListAdapter(new AppListAdapter(listView, getActivity(), null)); | ||||||
|             public boolean setViewValue(View view, Cursor cursor, int columnIndex) { |  | ||||||
|                 if (columnIndex == cursor.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)) { |  | ||||||
|                     String packageName = cursor.getString(columnIndex); |  | ||||||
|                     TextView textView = (TextView) view.findViewById(R.id.package_name); |  | ||||||
|                     textView.setText(packageName); |  | ||||||
|                     LinearLayout ll = (LinearLayout) view.getParent().getParent(); |  | ||||||
|                     ImageView iconView = (ImageView) ll.getChildAt(0); |  | ||||||
|                     Drawable icon; |  | ||||||
|                     try { |  | ||||||
|                         icon = packageManager.getApplicationIcon(packageName); |  | ||||||
|                     } catch (PackageManager.NameNotFoundException e) { |  | ||||||
|                         icon = defaultAppIcon; |  | ||||||
|                     } |  | ||||||
|                     iconView.setImageDrawable(icon); |  | ||||||
|                     return true; |  | ||||||
|                 } |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         setListAdapter(adapter); |  | ||||||
|         setListShown(false); // start out with a progress indicator |         setListShown(false); // start out with a progress indicator | ||||||
| 
 | 
 | ||||||
|         // either reconnect with an existing loader or start a new one |         // either reconnect with an existing loader or start a new one | ||||||
| @ -143,7 +112,7 @@ public class SelectAppsFragment extends ThemeableListFragment | |||||||
| 
 | 
 | ||||||
|         // build list of existing apps from what is on the file system |         // build list of existing apps from what is on the file system | ||||||
|         if (FDroidApp.selectedApps == null) { |         if (FDroidApp.selectedApps == null) { | ||||||
|             FDroidApp.selectedApps = new HashSet<String>(); |             FDroidApp.selectedApps = new HashSet<>(); | ||||||
|             for (String filename : LocalRepoManager.get(getActivity()).repoDir.list()) { |             for (String filename : LocalRepoManager.get(getActivity()).repoDir.list()) { | ||||||
|                 if (filename.matches(".*\\.apk")) { |                 if (filename.matches(".*\\.apk")) { | ||||||
|                     String packageName = filename.substring(0, filename.indexOf("_")); |                     String packageName = filename.substring(0, filename.indexOf("_")); | ||||||
| @ -155,7 +124,11 @@ public class SelectAppsFragment extends ThemeableListFragment | |||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onListItemClick(ListView l, View v, int position, long id) { |     public void onListItemClick(ListView l, View v, int position, long id) { | ||||||
|         Cursor c = (Cursor) l.getAdapter().getItem(position); |         toggleAppSelected(position); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void toggleAppSelected(int position) { | ||||||
|  |         Cursor c = (Cursor) getListAdapter().getItem(position); | ||||||
|         String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)); |         String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)); | ||||||
|         if (FDroidApp.selectedApps.contains(packageName)) { |         if (FDroidApp.selectedApps.contains(packageName)) { | ||||||
|             FDroidApp.selectedApps.remove(packageName); |             FDroidApp.selectedApps.remove(packageName); | ||||||
| @ -183,7 +156,7 @@ public class SelectAppsFragment extends ThemeableListFragment | |||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { |     public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { | ||||||
|         ((SimpleCursorAdapter) this.getListAdapter()).swapCursor(cursor); |         ((AppListAdapter)getListAdapter()).swapCursor(cursor); | ||||||
| 
 | 
 | ||||||
|         ListView listView = getListView(); |         ListView listView = getListView(); | ||||||
|         String fdroid = loader.getContext().getPackageName(); |         String fdroid = loader.getContext().getPackageName(); | ||||||
| @ -191,7 +164,7 @@ public class SelectAppsFragment extends ThemeableListFragment | |||||||
|             Cursor c = ((Cursor) listView.getItemAtPosition(i + 1)); |             Cursor c = ((Cursor) listView.getItemAtPosition(i + 1)); | ||||||
|             String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)); |             String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)); | ||||||
|             if (TextUtils.equals(packageName, fdroid)) { |             if (TextUtils.equals(packageName, fdroid)) { | ||||||
|                 listView.setItemChecked(i, true); // always include FDroid |                 listView.setItemChecked(i + 1, true); // always include FDroid | ||||||
|             } else { |             } else { | ||||||
|                 for (String selected : FDroidApp.selectedApps) { |                 for (String selected : FDroidApp.selectedApps) { | ||||||
|                     if (TextUtils.equals(packageName, selected)) { |                     if (TextUtils.equals(packageName, selected)) { | ||||||
| @ -210,7 +183,7 @@ public class SelectAppsFragment extends ThemeableListFragment | |||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onLoaderReset(Loader<Cursor> loader) { |     public void onLoaderReset(Loader<Cursor> loader) { | ||||||
|         ((SimpleCursorAdapter) this.getListAdapter()).swapCursor(null); |         ((AppListAdapter)getListAdapter()).swapCursor(null); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
| @ -233,10 +206,6 @@ public class SelectAppsFragment extends ThemeableListFragment | |||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public String getCurrentFilterString() { |  | ||||||
|         return mCurrentFilterString; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |     @Override | ||||||
|     protected int getThemeStyle() { |     protected int getThemeStyle() { | ||||||
|         return R.style.SwapTheme_StartSwap; |         return R.style.SwapTheme_StartSwap; | ||||||
| @ -246,4 +215,92 @@ public class SelectAppsFragment extends ThemeableListFragment | |||||||
|     protected int getHeaderLayout() { |     protected int getHeaderLayout() { | ||||||
|         return R.layout.swap_create_header; |         return R.layout.swap_create_header; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private class AppListAdapter extends CursorAdapter { | ||||||
|  | 
 | ||||||
|  |         @SuppressWarnings("UnusedDeclaration") | ||||||
|  |         private static final String TAG = "org.fdroid.fdroid.views.swap.SelectAppsFragment.AppListAdapter"; | ||||||
|  | 
 | ||||||
|  |         @Nullable | ||||||
|  |         private LayoutInflater inflater; | ||||||
|  | 
 | ||||||
|  |         @Nullable | ||||||
|  |         private Drawable defaultAppIcon; | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         private final ListView listView; | ||||||
|  | 
 | ||||||
|  |         public AppListAdapter(@NonNull ListView listView, @NonNull Context context, @Nullable Cursor c) { | ||||||
|  |             super(context, c, FLAG_REGISTER_CONTENT_OBSERVER); | ||||||
|  |             this.listView = listView; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         private LayoutInflater getInflater(Context context) { | ||||||
|  |             if (inflater == null) { | ||||||
|  |                 Context themedContext = new ContextThemeWrapper(context, R.style.SwapTheme_AppList_ListItem); | ||||||
|  |                 inflater = (LayoutInflater)themedContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); | ||||||
|  |             } | ||||||
|  |             return inflater; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         private Drawable getDefaultAppIcon(Context context) { | ||||||
|  |             if (defaultAppIcon == null) { | ||||||
|  |                 defaultAppIcon = context.getResources().getDrawable(android.R.drawable.sym_def_app_icon); | ||||||
|  |             } | ||||||
|  |             return defaultAppIcon; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public View newView(Context context, Cursor cursor, ViewGroup parent) { | ||||||
|  |             View view = getInflater(context).inflate(R.layout.select_local_apps_list_item, null); | ||||||
|  |             bindView(view, context, cursor); | ||||||
|  |             return view; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void bindView(final View view, final Context context, final Cursor cursor) { | ||||||
|  | 
 | ||||||
|  |             TextView packageView = (TextView)view.findViewById(R.id.package_name); | ||||||
|  |             TextView labelView = (TextView)view.findViewById(R.id.application_label); | ||||||
|  |             ImageView iconView = (ImageView)view.findViewById(android.R.id.icon); | ||||||
|  | 
 | ||||||
|  |             String packageName = cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)); | ||||||
|  |             String appLabel = cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.APPLICATION_LABEL)); | ||||||
|  | 
 | ||||||
|  |             Drawable icon; | ||||||
|  |             try { | ||||||
|  |                 icon = context.getPackageManager().getApplicationIcon(packageName); | ||||||
|  |             } catch (PackageManager.NameNotFoundException e) { | ||||||
|  |                 icon = getDefaultAppIcon(context); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             packageView.setText(packageName); | ||||||
|  |             labelView.setText(appLabel); | ||||||
|  |             iconView.setImageDrawable(icon); | ||||||
|  | 
 | ||||||
|  |             // Since v11, the Android SDK provided the ability to show selected list items | ||||||
|  |             // by highlighting their background. Prior to this, we need to handle this ourselves | ||||||
|  |             // by adding a checkbox which can toggle selected items. | ||||||
|  |             View checkBoxView = view.findViewById(R.id.checkbox); | ||||||
|  |             if (checkBoxView != null) { | ||||||
|  |                 CheckBox checkBox = (CheckBox)checkBoxView; | ||||||
|  |                 checkBox.setOnCheckedChangeListener(null); | ||||||
|  | 
 | ||||||
|  |                 final int cursorPosition = cursor.getPosition(); | ||||||
|  |                 final int listPosition = cursor.getPosition() + 1; // To account for the header view. | ||||||
|  | 
 | ||||||
|  |                 checkBox.setChecked(listView.isItemChecked(listPosition)); | ||||||
|  |                 checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { | ||||||
|  |                     @Override | ||||||
|  |                     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { | ||||||
|  |                         listView.setItemChecked(listPosition, isChecked); | ||||||
|  |                         toggleAppSelected(cursorPosition); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -5,6 +5,8 @@ import android.content.Context; | |||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import android.os.AsyncTask; | import android.os.AsyncTask; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.os.Handler; | ||||||
| import android.support.v4.app.Fragment; | import android.support.v4.app.Fragment; | ||||||
| import android.support.v4.app.FragmentManager; | import android.support.v4.app.FragmentManager; | ||||||
| import android.support.v7.app.ActionBarActivity; | import android.support.v7.app.ActionBarActivity; | ||||||
| @ -80,15 +82,24 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage | |||||||
| 
 | 
 | ||||||
|         if (savedInstanceState == null) { |         if (savedInstanceState == null) { | ||||||
| 
 | 
 | ||||||
|             showFragment(new StartSwapFragment(), STATE_START_SWAP); |             setContentView(R.layout.swap_activity); | ||||||
| 
 | 
 | ||||||
|             if (FDroidApp.isLocalRepoServiceRunning()) { |             // Necessary to run on an Android 2.3.[something] device. | ||||||
|                 showSelectApps(); |             new Handler().post(new Runnable() { | ||||||
|                 showJoinWifi(); |                 @Override | ||||||
|                 attemptToShowNfc(); |                 public void run() { | ||||||
|                 showWifiQr(); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|  |                     showFragment(new StartSwapFragment(), STATE_START_SWAP); | ||||||
|  | 
 | ||||||
|  |                     if (FDroidApp.isLocalRepoServiceRunning()) { | ||||||
|  |                         showSelectApps(); | ||||||
|  |                         showJoinWifi(); | ||||||
|  |                         attemptToShowNfc(); | ||||||
|  |                         showWifiQr(); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| @ -112,7 +123,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage | |||||||
|         // Even if they opted to skip the message which says "Touch devices to swap", |         // Even if they opted to skip the message which says "Touch devices to swap", | ||||||
|         // we still want to actually enable the feature, so that they could touch |         // we still want to actually enable the feature, so that they could touch | ||||||
|         // during the wifi qr code being shown too. |         // during the wifi qr code being shown too. | ||||||
|         boolean nfcMessageReady = NfcHelper.setPushMessage(this, Utils.getSharingUri(this, FDroidApp.repo)); |         boolean nfcMessageReady = NfcHelper.setPushMessage(this, Utils.getSharingUri(FDroidApp.repo)); | ||||||
| 
 | 
 | ||||||
|         if (Preferences.get().showNfcDuringSwap() && nfcMessageReady) { |         if (Preferences.get().showNfcDuringSwap() && nfcMessageReady) { | ||||||
|             showFragment(new NfcSwapFragment(), STATE_NFC); |             showFragment(new NfcSwapFragment(), STATE_NFC); | ||||||
| @ -133,7 +144,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage | |||||||
|     private void showFragment(Fragment fragment, String name) { |     private void showFragment(Fragment fragment, String name) { | ||||||
|         getSupportFragmentManager() |         getSupportFragmentManager() | ||||||
|                 .beginTransaction() |                 .beginTransaction() | ||||||
|                 .replace(android.R.id.content, fragment, name) |                 .replace(R.id.fragment_container, fragment, name) | ||||||
|                 .addToBackStack(name) |                 .addToBackStack(name) | ||||||
|                 .commit(); |                 .commit(); | ||||||
|     } |     } | ||||||
| @ -197,17 +208,25 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     class UpdateAsyncTask extends AsyncTask<Void, String, Void> { |     class UpdateAsyncTask extends AsyncTask<Void, String, Void> { | ||||||
|         private static final String TAG = "fdroid.SwapActivity.UpdateAsyncTask"; |  | ||||||
|         private ProgressDialog progressDialog; |  | ||||||
|         private Set<String> selectedApps; |  | ||||||
|         private Uri sharingUri; |  | ||||||
| 
 | 
 | ||||||
|         public UpdateAsyncTask(Context c, Set<String> apps) { |         @SuppressWarnings("UnusedDeclaration") | ||||||
|  |         private static final String TAG = "fdroid.SwapActivity.UpdateAsyncTask"; | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         private final ProgressDialog progressDialog; | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         private final Set<String> selectedApps; | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         private final Uri sharingUri; | ||||||
|  | 
 | ||||||
|  |         public UpdateAsyncTask(Context c, @NonNull Set<String> apps) { | ||||||
|             selectedApps = apps; |             selectedApps = apps; | ||||||
|             progressDialog = new ProgressDialog(c); |             progressDialog = new ProgressDialog(c); | ||||||
|             progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); |             progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); | ||||||
|             progressDialog.setTitle(R.string.updating); |             progressDialog.setTitle(R.string.updating); | ||||||
|             sharingUri = Utils.getSharingUri(c, FDroidApp.repo); |             sharingUri = Utils.getSharingUri(FDroidApp.repo); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         @Override |         @Override | ||||||
|  | |||||||
| @ -1,33 +1,79 @@ | |||||||
| package org.fdroid.fdroid.views.swap; | package org.fdroid.fdroid.views.swap; | ||||||
| 
 | 
 | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.content.Intent; | ||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
|  | import android.os.Handler; | ||||||
|  | import android.support.v4.app.NavUtils; | ||||||
| import android.support.annotation.Nullable; | import android.support.annotation.Nullable; | ||||||
| import android.support.v7.app.ActionBarActivity; | import android.support.v7.app.ActionBarActivity; | ||||||
|  | import android.util.Log; | ||||||
|  | 
 | ||||||
|  | import org.fdroid.fdroid.AppDetails; | ||||||
| import org.fdroid.fdroid.R; | import org.fdroid.fdroid.R; | ||||||
| import org.fdroid.fdroid.data.AppProvider; | import org.fdroid.fdroid.data.AppProvider; | ||||||
|  | import org.fdroid.fdroid.data.Repo; | ||||||
|  | import org.fdroid.fdroid.data.RepoProvider; | ||||||
| import org.fdroid.fdroid.views.AppListAdapter; | import org.fdroid.fdroid.views.AppListAdapter; | ||||||
| import org.fdroid.fdroid.views.AvailableAppListAdapter; | import org.fdroid.fdroid.views.AvailableAppListAdapter; | ||||||
| import org.fdroid.fdroid.views.fragments.AppListFragment; | import org.fdroid.fdroid.views.fragments.AppListFragment; | ||||||
| 
 | 
 | ||||||
| public class SwapAppListActivity extends ActionBarActivity { | public class SwapAppListActivity extends ActionBarActivity { | ||||||
| 
 | 
 | ||||||
|  |     private static final String TAG = "fdroid.SwapAppListActivity"; | ||||||
|  | 
 | ||||||
|  |     public static String EXTRA_REPO_ID = "repoId"; | ||||||
|  | 
 | ||||||
|  |     private Repo repo; | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onCreate(Bundle savedInstanceState) { |     public void onCreate(Bundle savedInstanceState) { | ||||||
| 
 | 
 | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
| 
 | 
 | ||||||
|         if (savedInstanceState == null) { |         if (savedInstanceState == null) { | ||||||
|             getSupportFragmentManager() | 
 | ||||||
|                     .beginTransaction() |             // Necessary to run on an Android 2.3.[something] device. | ||||||
|                     .add(android.R.id.content, new SwapAppListFragment()) |             new Handler().post(new Runnable() { | ||||||
|                     .commit(); |                 @Override | ||||||
|  |                 public void run() { | ||||||
|  |                     getSupportFragmentManager() | ||||||
|  |                         .beginTransaction() | ||||||
|  |                         .add(android.R.id.content, new SwapAppListFragment()) | ||||||
|  |                         .commit(); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onResume() { | ||||||
|  |         super.onResume(); | ||||||
|  | 
 | ||||||
|  |         long repoAddress = getIntent().getLongExtra(EXTRA_REPO_ID, -1); | ||||||
|  |         repo = RepoProvider.Helper.findById(this, repoAddress); | ||||||
|  |         if (repo == null) { | ||||||
|  |             Log.e(TAG, "Couldn't show swap app list for repo " + repoAddress); | ||||||
|  |             finish(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Repo getRepo() { | ||||||
|  |         return repo; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public static class SwapAppListFragment extends AppListFragment { |     public static class SwapAppListFragment extends AppListFragment { | ||||||
| 
 | 
 | ||||||
|  |         private Repo repo; | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void onAttach(Activity activity) { | ||||||
|  |             super.onAttach(activity); | ||||||
|  |             repo = ((SwapAppListActivity)activity).getRepo(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         @Override |         @Override | ||||||
|         protected int getHeaderLayout() { |         protected int getHeaderLayout() { | ||||||
|             return R.layout.swap_success_header; |             return R.layout.swap_success_header; | ||||||
| @ -51,7 +97,39 @@ public class SwapAppListActivity extends ActionBarActivity { | |||||||
| 
 | 
 | ||||||
|         @Override |         @Override | ||||||
|         protected Uri getDataUri() { |         protected Uri getDataUri() { | ||||||
|             return AppProvider.getCategoryUri("LocalRepo"); |             return AppProvider.getRepoUri(repo); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         protected Intent getAppDetailsIntent() { | ||||||
|  |             Intent intent = new Intent(getActivity(), SwapAppDetails.class); | ||||||
|  |             intent.putExtra(EXTRA_REPO_ID, repo.getId()); | ||||||
|  |             return intent; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Only difference from base class is that it navigates up to a different task. | ||||||
|  |      * It will go to the {@link org.fdroid.fdroid.views.swap.SwapAppListActivity} | ||||||
|  |      * whereas the baseclass will go back to the main list of apps. Need to juggle | ||||||
|  |      * the repoId in order to be able to return to an appropriately configured swap | ||||||
|  |      * list (see {@link org.fdroid.fdroid.views.swap.SwapAppListActivity.SwapAppListFragment#getAppDetailsIntent()}). | ||||||
|  |      */ | ||||||
|  |     public static class SwapAppDetails extends AppDetails { | ||||||
|  | 
 | ||||||
|  |         private long repoId; | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         protected void onResume() { | ||||||
|  |             super.onResume(); | ||||||
|  |             repoId = getIntent().getLongExtra(EXTRA_REPO_ID, -1); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         protected void navigateUp() { | ||||||
|  |             Intent parentIntent = NavUtils.getParentActivityIntent(this); | ||||||
|  |             parentIntent.putExtra(EXTRA_REPO_ID, repoId); | ||||||
|  |             NavUtils.navigateUpTo(this, parentIntent); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,5 +1,11 @@ | |||||||
| package org.fdroid.fdroid.views.swap; | package org.fdroid.fdroid.views.swap; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Defines the contract between the {@link org.fdroid.fdroid.views.swap.SwapActivity} | ||||||
|  |  * and the fragments which live in it. The fragments each have the responsibility of | ||||||
|  |  * moving to the next stage of the process, and are entitled to stop swapping too | ||||||
|  |  * (e.g. when a "Cancel" button is pressed). | ||||||
|  |  */ | ||||||
| public interface SwapProcessManager { | public interface SwapProcessManager { | ||||||
|     public void nextStep(); |     public void nextStep(); | ||||||
|     public void stopSwapping(); |     public void stopSwapping(); | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| package org.fdroid.fdroid.views.swap; | package org.fdroid.fdroid.views.swap; | ||||||
| 
 | 
 | ||||||
| import android.annotation.TargetApi; |  | ||||||
| import android.app.Activity; | import android.app.Activity; | ||||||
| import android.content.BroadcastReceiver; | import android.content.BroadcastReceiver; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| @ -23,6 +22,8 @@ import android.widget.TextView; | |||||||
| import android.widget.Toast; | import android.widget.Toast; | ||||||
| import com.google.zxing.integration.android.IntentIntegrator; | import com.google.zxing.integration.android.IntentIntegrator; | ||||||
| import com.google.zxing.integration.android.IntentResult; | import com.google.zxing.integration.android.IntentResult; | ||||||
|  | import org.apache.http.NameValuePair; | ||||||
|  | import org.apache.http.client.utils.URLEncodedUtils; | ||||||
| import org.fdroid.fdroid.FDroid; | import org.fdroid.fdroid.FDroid; | ||||||
| import org.fdroid.fdroid.FDroidApp; | import org.fdroid.fdroid.FDroidApp; | ||||||
| import org.fdroid.fdroid.Preferences; | import org.fdroid.fdroid.Preferences; | ||||||
| @ -32,12 +33,16 @@ import org.fdroid.fdroid.Utils; | |||||||
| import org.fdroid.fdroid.data.NewRepoConfig; | import org.fdroid.fdroid.data.NewRepoConfig; | ||||||
| import org.fdroid.fdroid.net.WifiStateChangeService; | import org.fdroid.fdroid.net.WifiStateChangeService; | ||||||
| 
 | 
 | ||||||
|  | import java.net.URI; | ||||||
|  | import java.util.List; | ||||||
| import java.util.Locale; | import java.util.Locale; | ||||||
| 
 | 
 | ||||||
| public class WifiQrFragment extends Fragment { | public class WifiQrFragment extends Fragment { | ||||||
| 
 | 
 | ||||||
|     private static final int CONNECT_TO_SWAP = 1; |     private static final int CONNECT_TO_SWAP = 1; | ||||||
| 
 | 
 | ||||||
|  |     private static final String TAG = "org.fdroid.fdroid.views.swap.WifiQrFragment"; | ||||||
|  | 
 | ||||||
|     private BroadcastReceiver onWifiChange = new BroadcastReceiver() { |     private BroadcastReceiver onWifiChange = new BroadcastReceiver() { | ||||||
|         @Override |         @Override | ||||||
|         public void onReceive(Context context, Intent i) { |         public void onReceive(Context context, Intent i) { | ||||||
| @ -105,10 +110,9 @@ public class WifiQrFragment extends Fragment { | |||||||
|                 new IntentFilter(WifiStateChangeService.BROADCAST)); |                 new IntentFilter(WifiStateChangeService.BROADCAST)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @TargetApi(14) |  | ||||||
|     private void setUIFromWifi() { |     private void setUIFromWifi() { | ||||||
| 
 | 
 | ||||||
|         if (TextUtils.isEmpty(FDroidApp.repo.address)) |         if (TextUtils.isEmpty(FDroidApp.repo.address) || getView() == null) | ||||||
|             return; |             return; | ||||||
| 
 | 
 | ||||||
|         String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https://" : "http://"; |         String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https://" : "http://"; | ||||||
| @ -125,26 +129,33 @@ public class WifiQrFragment extends Fragment { | |||||||
|          * wifi AP to join. Lots of QR Scanners are buggy and do not respect |          * wifi AP to join. Lots of QR Scanners are buggy and do not respect | ||||||
|          * custom URI schemes, so we have to use http:// or https:// :-( |          * custom URI schemes, so we have to use http:// or https:// :-( | ||||||
|          */ |          */ | ||||||
|         Uri sharingUri = Utils.getSharingUri(getActivity(), FDroidApp.repo); |         Uri sharingUri = Utils.getSharingUri(FDroidApp.repo); | ||||||
|         String qrUriString = (scheme + sharingUri.getHost()).toUpperCase(Locale.ENGLISH); |         String qrUriString = (scheme + sharingUri.getHost()).toUpperCase(Locale.ENGLISH); | ||||||
|         if (sharingUri.getPort() != 80) { |         if (sharingUri.getPort() != 80) { | ||||||
|             qrUriString += ":" + sharingUri.getPort(); |             qrUriString += ":" + sharingUri.getPort(); | ||||||
|         } |         } | ||||||
|         qrUriString += sharingUri.getPath().toUpperCase(Locale.ENGLISH); |         qrUriString += sharingUri.getPath().toUpperCase(Locale.ENGLISH); | ||||||
|         boolean first = true; |         boolean first = true; | ||||||
|         for (String parameterName : sharingUri.getQueryParameterNames()) { | 
 | ||||||
|             if (!parameterName.equals("ssid")) { |         // Andorid provides an API for getting the query parameters and iterating over them: | ||||||
|  |         //   Uri.getQueryParameterNames() | ||||||
|  |         // But it is only available on later Android versions. As such we use URLEncodedUtils instead. | ||||||
|  |         List<NameValuePair> parameters = URLEncodedUtils.parse(URI.create(sharingUri.toString()), "UTF-8"); | ||||||
|  |         for (NameValuePair parameter : parameters) { | ||||||
|  |             if (!parameter.getName().equals("ssid")) { | ||||||
|                 if (first) { |                 if (first) { | ||||||
|                     qrUriString += "?"; |                     qrUriString += "?"; | ||||||
|                     first = false; |                     first = false; | ||||||
|                 } else { |                 } else { | ||||||
|                     qrUriString += "&"; |                     qrUriString += "&"; | ||||||
|                 } |                 } | ||||||
|                 qrUriString += parameterName.toUpperCase(Locale.ENGLISH) + "=" + |                 qrUriString += parameter.getName().toUpperCase(Locale.ENGLISH) + "=" + | ||||||
|                     sharingUri.getQueryParameter(parameterName).toUpperCase(Locale.ENGLISH); |                     parameter.getValue().toUpperCase(Locale.ENGLISH); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         Log.i(TAG, "Encoded swap URI in QR Code: " + qrUriString); | ||||||
|  | 
 | ||||||
|         // zxing requires >= 8 |         // zxing requires >= 8 | ||||||
|         // TODO: What about 7? I don't feel comfortable bumping the min version for this... |         // TODO: What about 7? I don't feel comfortable bumping the min version for this... | ||||||
|         // I would suggest show some alternate info, with directions for how to add a new repository manually. |         // I would suggest show some alternate info, with directions for how to add a new repository manually. | ||||||
|  | |||||||
							
								
								
									
										19
									
								
								F-Droid/test/src/mock/MockApplicationInfo.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								F-Droid/test/src/mock/MockApplicationInfo.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | package mock; | ||||||
|  | 
 | ||||||
|  | import android.content.pm.ApplicationInfo; | ||||||
|  | import android.content.pm.PackageInfo; | ||||||
|  | import android.content.pm.PackageManager; | ||||||
|  | 
 | ||||||
|  | public class MockApplicationInfo extends ApplicationInfo { | ||||||
|  | 
 | ||||||
|  |     private final PackageInfo info; | ||||||
|  | 
 | ||||||
|  |     public MockApplicationInfo(PackageInfo info) { | ||||||
|  |         this.info = info; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public CharSequence loadLabel(PackageManager pm) { | ||||||
|  |         return "Mock app: " + info.packageName; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -1,5 +1,6 @@ | |||||||
| package mock; | package mock; | ||||||
| 
 | 
 | ||||||
|  | import android.content.pm.ApplicationInfo; | ||||||
| import android.content.pm.PackageInfo; | import android.content.pm.PackageInfo; | ||||||
| import android.test.mock.MockPackageManager; | import android.test.mock.MockPackageManager; | ||||||
| 
 | 
 | ||||||
| @ -9,7 +10,7 @@ import java.util.List; | |||||||
| 
 | 
 | ||||||
| public class MockInstallablePackageManager extends MockPackageManager { | public class MockInstallablePackageManager extends MockPackageManager { | ||||||
| 
 | 
 | ||||||
|     private List<PackageInfo> info = new ArrayList<PackageInfo>(); |     private List<PackageInfo> info = new ArrayList<>(); | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public List<PackageInfo> getInstalledPackages(int flags) { |     public List<PackageInfo> getInstalledPackages(int flags) { | ||||||
| @ -30,6 +31,11 @@ public class MockInstallablePackageManager extends MockPackageManager { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public ApplicationInfo getApplicationInfo(String packageName, int flags) throws NameNotFoundException { | ||||||
|  |         return new MockApplicationInfo(getPackageInfo(packageName)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public PackageInfo getPackageInfo(String id) { |     public PackageInfo getPackageInfo(String id) { | ||||||
|         for (PackageInfo i : info) { |         for (PackageInfo i : info) { | ||||||
|             if (i.packageName.equals(id)) { |             if (i.packageName.equals(id)) { | ||||||
|  | |||||||
| @ -1,14 +1,18 @@ | |||||||
| package org.fdroid.fdroid; | package org.fdroid.fdroid; | ||||||
| 
 | 
 | ||||||
|  | import android.content.ContentResolver; | ||||||
| import android.content.ContentValues; | import android.content.ContentValues; | ||||||
| import android.content.res.Resources; | import android.content.res.Resources; | ||||||
| import android.database.Cursor; | import android.database.Cursor; | ||||||
| 
 | 
 | ||||||
| import mock.MockCategoryResources; | import mock.MockCategoryResources; | ||||||
|  | import mock.MockContextSwappableComponents; | ||||||
|  | import mock.MockInstallablePackageManager; | ||||||
| 
 | 
 | ||||||
| import org.fdroid.fdroid.data.ApkProvider; | import org.fdroid.fdroid.data.ApkProvider; | ||||||
| import org.fdroid.fdroid.data.App; | import org.fdroid.fdroid.data.App; | ||||||
| import org.fdroid.fdroid.data.AppProvider; | import org.fdroid.fdroid.data.AppProvider; | ||||||
|  | import org.fdroid.fdroid.data.InstalledAppCacheUpdater; | ||||||
| 
 | 
 | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| @ -43,7 +47,6 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> { | |||||||
|      * the AppProvider used to stumble across this bug when asking for installed apps, |      * the AppProvider used to stumble across this bug when asking for installed apps, | ||||||
|      * and the device had over 1000 apps installed. |      * and the device had over 1000 apps installed. | ||||||
|      */ |      */ | ||||||
| /* TODO fix me |  | ||||||
|     public void testMaxSqliteParams() { |     public void testMaxSqliteParams() { | ||||||
| 
 | 
 | ||||||
|         MockInstallablePackageManager pm = new MockInstallablePackageManager(); |         MockInstallablePackageManager pm = new MockInstallablePackageManager(); | ||||||
| @ -74,7 +77,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> { | |||||||
| 
 | 
 | ||||||
|         assertResultCount(3, AppProvider.getInstalledUri()); |         assertResultCount(3, AppProvider.getInstalledUri()); | ||||||
|     } |     } | ||||||
| */ | 
 | ||||||
|     public void testCantFindApp() { |     public void testCantFindApp() { | ||||||
|         assertNull(AppProvider.Helper.findById(getMockContentResolver(), "com.example.doesnt-exist")); |         assertNull(AppProvider.Helper.findById(getMockContentResolver(), "com.example.doesnt-exist")); | ||||||
|     } |     } | ||||||
| @ -92,7 +95,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> { | |||||||
|         App app = new App(); |         App app = new App(); | ||||||
|         app.id = "org.fdroid.fdroid"; |         app.id = "org.fdroid.fdroid"; | ||||||
| 
 | 
 | ||||||
|         List<App> apps = new ArrayList<App>(1); |         List<App> apps = new ArrayList<>(1); | ||||||
|         apps.add(app); |         apps.add(app); | ||||||
| 
 | 
 | ||||||
|         assertValidUri(AppProvider.getContentUri(app)); |         assertValidUri(AppProvider.getContentUri(app)); | ||||||
| @ -105,7 +108,6 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> { | |||||||
|         assertNotNull(cursor); |         assertNotNull(cursor); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| /* TODO fix me |  | ||||||
|     private void insertApps(int count) { |     private void insertApps(int count) { | ||||||
|         for (int i = 0; i < count; i ++) { |         for (int i = 0; i < count; i ++) { | ||||||
|             insertApp("com.example.test." + i, "Test app " + i); |             insertApp("com.example.test." + i, "Test app " + i); | ||||||
| @ -122,7 +124,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> { | |||||||
|         values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVercode); |         values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVercode); | ||||||
|         insertApp(id, "App: " + id, values); |         insertApp(id, "App: " + id, values); | ||||||
| 
 | 
 | ||||||
|         TestUtils.installAndBroadcast(getMockContext(), packageManager, id, installedVercode, "v" + installedVercode); |         TestUtils.installAndBroadcast(getSwappableContext(), packageManager, id, installedVercode, "v" + installedVercode); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void testCanUpdate() { |     public void testCanUpdate() { | ||||||
| @ -173,7 +175,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> { | |||||||
| 
 | 
 | ||||||
|         Cursor canUpdateCursor = r.query(AppProvider.getCanUpdateUri(), AppProvider.DataColumns.ALL, null, null, null); |         Cursor canUpdateCursor = r.query(AppProvider.getCanUpdateUri(), AppProvider.DataColumns.ALL, null, null, null); | ||||||
|         canUpdateCursor.moveToFirst(); |         canUpdateCursor.moveToFirst(); | ||||||
|         List<String> canUpdateIds = new ArrayList<String>(canUpdateCursor.getCount()); |         List<String> canUpdateIds = new ArrayList<>(canUpdateCursor.getCount()); | ||||||
|         while (!canUpdateCursor.isAfterLast()) { |         while (!canUpdateCursor.isAfterLast()) { | ||||||
|             canUpdateIds.add(new App(canUpdateCursor).id); |             canUpdateIds.add(new App(canUpdateCursor).id); | ||||||
|             canUpdateCursor.moveToNext(); |             canUpdateCursor.moveToNext(); | ||||||
| @ -224,7 +226,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void assertContainsOnlyIds(List<App> actualApps, String[] expectedIds) { |     private void assertContainsOnlyIds(List<App> actualApps, String[] expectedIds) { | ||||||
|         List<String> actualIds = new ArrayList<String>(actualApps.size()); |         List<String> actualIds = new ArrayList<>(actualApps.size()); | ||||||
|         for (App app : actualApps) { |         for (App app : actualApps) { | ||||||
|             actualIds.add(app.id); |             actualIds.add(app.id); | ||||||
|         } |         } | ||||||
| @ -241,12 +243,11 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> { | |||||||
|         assertResultCount(0, AppProvider.getInstalledUri()); |         assertResultCount(0, AppProvider.getInstalledUri()); | ||||||
| 
 | 
 | ||||||
|         for (int i = 10; i < 20; i ++) { |         for (int i = 10; i < 20; i ++) { | ||||||
|             TestUtils.installAndBroadcast(getMockContext(), pm, "com.example.test." + i, i, "v1"); |             TestUtils.installAndBroadcast(getSwappableContext(), pm, "com.example.test." + i, i, "v1"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         assertResultCount(10, AppProvider.getInstalledUri()); |         assertResultCount(10, AppProvider.getInstalledUri()); | ||||||
|     } |     } | ||||||
| */ |  | ||||||
| 
 | 
 | ||||||
|     public void testInsert() { |     public void testInsert() { | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -25,8 +25,13 @@ public class FileCompatTest extends InstrumentationTestCase { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void tearDown() { |     public void tearDown() { | ||||||
|         assertTrue("Can't delete " + sourceFile.getAbsolutePath() + ".", sourceFile.delete()); |         if (sourceFile.exists()) { | ||||||
|         assertTrue("Can't delete " + destFile.getAbsolutePath() + ".", destFile.delete()); |             assertTrue("Can't delete " + sourceFile.getAbsolutePath() + ".", sourceFile.delete()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (destFile.exists()) { | ||||||
|  |             assertTrue("Can't delete " + destFile.getAbsolutePath() + ".", destFile.delete()); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void testSymlinkRuntime() { |     public void testSymlinkRuntime() { | ||||||
|  | |||||||
| @ -37,7 +37,6 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro | |||||||
|         assertValidUri(InstalledAppProvider.getAppUri("blah")); |         assertValidUri(InstalledAppProvider.getAppUri("blah")); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| /* TODO fix me |  | ||||||
|     public void testInsert() { |     public void testInsert() { | ||||||
| 
 | 
 | ||||||
|         assertResultCount(0, InstalledAppProvider.getContentUri()); |         assertResultCount(0, InstalledAppProvider.getContentUri()); | ||||||
| @ -134,7 +133,7 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro | |||||||
|         assertIsInstalledVersionInDb("com.example.toKeep", 1, "v0.1"); |         assertIsInstalledVersionInDb("com.example.toKeep", 1, "v0.1"); | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| */ | 
 | ||||||
|     @Override |     @Override | ||||||
|     protected String[] getMinimalProjection() { |     protected String[] getMinimalProjection() { | ||||||
|         return new String[] { |         return new String[] { | ||||||
| @ -153,6 +152,7 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro | |||||||
|         if (appId != null) { |         if (appId != null) { | ||||||
|             values.put(InstalledAppProvider.DataColumns.APP_ID, appId); |             values.put(InstalledAppProvider.DataColumns.APP_ID, appId); | ||||||
|         } |         } | ||||||
|  |         values.put(InstalledAppProvider.DataColumns.APPLICATION_LABEL, "Mock app: " + appId); | ||||||
|         values.put(InstalledAppProvider.DataColumns.VERSION_CODE, versionCode); |         values.put(InstalledAppProvider.DataColumns.VERSION_CODE, versionCode); | ||||||
|         values.put(InstalledAppProvider.DataColumns.VERSION_NAME, versionNumber); |         values.put(InstalledAppProvider.DataColumns.VERSION_NAME, versionNumber); | ||||||
|         return values; |         return values; | ||||||
| @ -164,15 +164,15 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void removeAndBroadcast(String appId) { |     private void removeAndBroadcast(String appId) { | ||||||
|         TestUtils.removeAndBroadcast(getMockContext(), getPackageManager(), appId); |         TestUtils.removeAndBroadcast(getSwappableContext(), getPackageManager(), appId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void upgradeAndBroadcast(String appId, int versionCode, String versionName) { |     private void upgradeAndBroadcast(String appId, int versionCode, String versionName) { | ||||||
|         TestUtils.upgradeAndBroadcast(getMockContext(), getPackageManager(), appId, versionCode, versionName); |         TestUtils.upgradeAndBroadcast(getSwappableContext(), getPackageManager(), appId, versionCode, versionName); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void installAndBroadcast(String appId, int versionCode, String versionName) { |     private void installAndBroadcast(String appId, int versionCode, String versionName) { | ||||||
|         TestUtils.installAndBroadcast(getMockContext(), getPackageManager(), appId, versionCode, versionName); |         TestUtils.installAndBroadcast(getSwappableContext(), getPackageManager(), appId, versionCode, versionName); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,9 +6,14 @@ import android.net.Uri; | |||||||
| import android.os.Environment; | import android.os.Environment; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import junit.framework.AssertionFailedError; | import junit.framework.AssertionFailedError; | ||||||
|  | 
 | ||||||
|  | import mock.MockContextSwappableComponents; | ||||||
| import mock.MockInstallablePackageManager; | import mock.MockInstallablePackageManager; | ||||||
| import org.fdroid.fdroid.data.ApkProvider; | import org.fdroid.fdroid.data.ApkProvider; | ||||||
| import org.fdroid.fdroid.data.AppProvider; | import org.fdroid.fdroid.data.AppProvider; | ||||||
|  | import org.fdroid.fdroid.receiver.PackageAddedReceiver; | ||||||
|  | import org.fdroid.fdroid.receiver.PackageRemovedReceiver; | ||||||
|  | import org.fdroid.fdroid.receiver.PackageUpgradedReceiver; | ||||||
| 
 | 
 | ||||||
| import java.io.*; | import java.io.*; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| @ -17,7 +22,7 @@ import java.util.List; | |||||||
| 
 | 
 | ||||||
| public class TestUtils { | public class TestUtils { | ||||||
| 
 | 
 | ||||||
|     private static final String TAG = "org.fdroid.fdroid.TestUtils"; |     private static final String TAG = "fdroid.TestUtils"; | ||||||
| 
 | 
 | ||||||
|     public static <T extends Comparable> void assertContainsOnly(List<T> actualList, T[] expectedArray) { |     public static <T extends Comparable> void assertContainsOnly(List<T> actualList, T[] expectedArray) { | ||||||
|         List<T> expectedList = new ArrayList<T>(expectedArray.length); |         List<T> expectedList = new ArrayList<T>(expectedArray.length); | ||||||
| @ -132,19 +137,14 @@ public class TestUtils { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Will tell {@code pm} that we are installing {@code appId}, and then alert the |      * Will tell {@code pm} that we are installing {@code appId}, and then alert the | ||||||
|      * {@link org.fdroid.fdroid.PackageAddedReceiver}. This will in turn update the |      * {@link org.fdroid.fdroid.receiver.PackageAddedReceiver}. This will in turn update the | ||||||
|      * "installed apps" table in the database. |      * "installed apps" table in the database. | ||||||
|      * |  | ||||||
|      * Note: in order for this to work, the {@link AppProviderTest#getSwappableContext()} |  | ||||||
|      * will need to be aware of the package manager that we have passed in. Therefore, |  | ||||||
|      * you will have to have called |  | ||||||
|      * {@link mock.MockContextSwappableComponents#setPackageManager(android.content.pm.PackageManager)} |  | ||||||
|      * on the {@link AppProviderTest#getSwappableContext()} before invoking this method. |  | ||||||
|      */ |      */ | ||||||
|     public static void installAndBroadcast( |     public static void installAndBroadcast( | ||||||
|             Context context,  MockInstallablePackageManager pm, |             MockContextSwappableComponents context,  MockInstallablePackageManager pm, | ||||||
|             String appId, int versionCode, String versionName) { |             String appId, int versionCode, String versionName) { | ||||||
| 
 | 
 | ||||||
|  |         context.setPackageManager(pm); | ||||||
|         pm.install(appId, versionCode, versionName); |         pm.install(appId, versionCode, versionName); | ||||||
|         Intent installIntent = new Intent(Intent.ACTION_PACKAGE_ADDED); |         Intent installIntent = new Intent(Intent.ACTION_PACKAGE_ADDED); | ||||||
|         installIntent.setData(Uri.parse("package:" + appId)); |         installIntent.setData(Uri.parse("package:" + appId)); | ||||||
| @ -153,15 +153,16 @@ public class TestUtils { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(android.content.Context context, mock.MockInstallablePackageManager, String, int, String) |      * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(mock.MockContextSwappableComponents, mock.MockInstallablePackageManager, String, int, String) | ||||||
|      */ |      */ | ||||||
|     public static void upgradeAndBroadcast( |     public static void upgradeAndBroadcast( | ||||||
|             Context context, MockInstallablePackageManager pm, |             MockContextSwappableComponents context, MockInstallablePackageManager pm, | ||||||
|             String appId, int versionCode, String versionName) { |             String appId, int versionCode, String versionName) { | ||||||
|         /* |         /* | ||||||
|         removeAndBroadcast(context, pm, appId); |         removeAndBroadcast(context, pm, appId); | ||||||
|         installAndBroadcast(context, pm, appId, versionCode, versionName); |         installAndBroadcast(context, pm, appId, versionCode, versionName); | ||||||
|         */ |         */ | ||||||
|  |         context.setPackageManager(pm); | ||||||
|         pm.install(appId, versionCode, versionName); |         pm.install(appId, versionCode, versionName); | ||||||
|         Intent installIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED); |         Intent installIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED); | ||||||
|         installIntent.setData(Uri.parse("package:" + appId)); |         installIntent.setData(Uri.parse("package:" + appId)); | ||||||
| @ -170,10 +171,11 @@ public class TestUtils { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(android.content.Context context, mock.MockInstallablePackageManager, String, int, String) |      * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(mock.MockContextSwappableComponents, mock.MockInstallablePackageManager, String, int, String) | ||||||
|      */ |      */ | ||||||
|     public static void removeAndBroadcast(Context context, MockInstallablePackageManager pm, String appId) { |     public static void removeAndBroadcast(MockContextSwappableComponents context, MockInstallablePackageManager pm, String appId) { | ||||||
| 
 | 
 | ||||||
|  |         context.setPackageManager(pm); | ||||||
|         pm.remove(appId); |         pm.remove(appId); | ||||||
|         Intent installIntent = new Intent(Intent.ACTION_PACKAGE_REMOVED); |         Intent installIntent = new Intent(Intent.ACTION_PACKAGE_REMOVED); | ||||||
|         installIntent.setData(Uri.parse("package:" + appId)); |         installIntent.setData(Uri.parse("package:" + appId)); | ||||||
|  | |||||||
| @ -3,21 +3,15 @@ package org.fdroid.fdroid.updater; | |||||||
| 
 | 
 | ||||||
| import android.annotation.TargetApi; | import android.annotation.TargetApi; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.os.Environment; |  | ||||||
| import android.test.InstrumentationTestCase; | import android.test.InstrumentationTestCase; | ||||||
| import android.util.Log; |  | ||||||
| 
 | 
 | ||||||
| import org.apache.commons.io.FileUtils; | import org.apache.commons.io.FileUtils; | ||||||
| import org.fdroid.fdroid.TestUtils; | import org.fdroid.fdroid.TestUtils; | ||||||
| import org.fdroid.fdroid.Utils; |  | ||||||
| import org.fdroid.fdroid.data.Repo; | import org.fdroid.fdroid.data.Repo; | ||||||
| import org.fdroid.fdroid.updater.RepoUpdater.UpdateException; | import org.fdroid.fdroid.updater.RepoUpdater.UpdateException; | ||||||
| 
 | 
 | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.FileOutputStream; |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.InputStream; |  | ||||||
| import java.io.OutputStream; |  | ||||||
| 
 | 
 | ||||||
| @TargetApi(8) | @TargetApi(8) | ||||||
| public class SignedRepoUpdaterTest extends InstrumentationTestCase { | public class SignedRepoUpdaterTest extends InstrumentationTestCase { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Peter Serwylo
						Peter Serwylo