From 4f7f7b2cb5d75a550d66856b17016151a4f56ee8 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Sat, 23 May 2015 00:08:24 +1000 Subject: [PATCH] WIP: Refactored select apps to swap into view. Not worrying about styling yet, just functionality. Added an InnerView interface that these views can implement. Currently it asks them to populate the menu. It may be slightly inefficient if we end up with a popup menu, because it is called onPrepareOptionsMenu, but expects the inner view to inflate the menu. However, for swap this shouldn't be an issue, as all the menus pretty much fit in the action bar of most screen sizes. --- F-Droid/AndroidManifest.xml | 11 + .../select_local_apps_list_item.xml | 2 +- .../layout/select_local_apps_list_item.xml | 2 +- F-Droid/res/layout/swap_select_apps.xml | 9 + F-Droid/src/org/fdroid/fdroid/FDroid.java | 3 +- .../views/swap/SwapWorkflowActivity.java | 185 +++++++++++ .../views/swap/views/SelectAppsView.java | 302 ++++++++++++++++++ .../views/swap/views/StartSwapView.java | 18 +- 8 files changed, 522 insertions(+), 10 deletions(-) create mode 100644 F-Droid/res/layout/swap_select_apps.xml create mode 100644 F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java create mode 100644 F-Droid/src/org/fdroid/fdroid/views/swap/views/SelectAppsView.java diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml index 63c9900de..be3883ec7 100644 --- a/F-Droid/AndroidManifest.xml +++ b/F-Droid/AndroidManifest.xml @@ -396,6 +396,17 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".FDroid" /> + + + + android:paddingTop="2dip"> + android:paddingTop="2dip"> + + + + \ No newline at end of file diff --git a/F-Droid/src/org/fdroid/fdroid/FDroid.java b/F-Droid/src/org/fdroid/fdroid/FDroid.java index a532dbbc2..a04e3d4ba 100644 --- a/F-Droid/src/org/fdroid/fdroid/FDroid.java +++ b/F-Droid/src/org/fdroid/fdroid/FDroid.java @@ -51,6 +51,7 @@ import org.fdroid.fdroid.views.AppListFragmentPagerAdapter; import org.fdroid.fdroid.views.ManageReposActivity; import org.fdroid.fdroid.views.swap.ConnectSwapActivity; import org.fdroid.fdroid.views.swap.SwapActivity; +import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; public class FDroid extends ActionBarActivity { @@ -257,7 +258,7 @@ public class FDroid extends ActionBarActivity { return true; case R.id.action_swap: - startActivity(new Intent(this, SwapActivity.class)); + startActivity(new Intent(this, SwapWorkflowActivity.class)); return true; case R.id.action_search: diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java new file mode 100644 index 000000000..33f41643a --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -0,0 +1,185 @@ +package org.fdroid.fdroid.views.swap; + +import android.app.ProgressDialog; +import android.content.Context; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.v4.app.FragmentActivity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.localrepo.LocalRepoManager; + +import java.util.Set; + +public class SwapWorkflowActivity extends FragmentActivity { + + private ViewGroup container; + + private enum State { + INTRO, SELECT_APPS, JOIN_WIFI + } + + public interface InnerView { + /** @return True if the menu should be shown. */ + boolean buildMenu(Menu menu, @NonNull MenuInflater inflater); + } + + private State currentState = State.INTRO; + private InnerView currentView; + private boolean hasPreparedLocalRepo = false; + private UpdateAsyncTask updateSwappableAppsTask = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.swap_activity); + container = (ViewGroup) findViewById(R.id.fragment_container); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + boolean parent = super.onPrepareOptionsMenu(menu); + boolean inner = currentView.buildMenu(menu, getMenuInflater()); + return parent || inner; + } + + @Override + protected void onResume() { + super.onResume(); + showView(); + } + + private void showView() { + if (currentState == State.INTRO) { + showIntro(); + } + } + + private void inflateInnerView(@LayoutRes int viewRes) { + container.removeAllViews(); + View view = ((LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(viewRes, container, false); + currentView = (InnerView)view; + container.addView(view); + supportInvalidateOptionsMenu(); + } + + private void showIntro() { + inflateInnerView(R.layout.swap_blank); + } + + public void showSelectApps() { + inflateInnerView(R.layout.swap_select_apps); + } + + // TODO: Pass in the selected apps, then we can figure out whether they have changed. + public void onAppsSelected(Set selectedApps) { + if (updateSwappableAppsTask == null && !hasPreparedLocalRepo) { + updateSwappableAppsTask = new UpdateAsyncTask(this, selectedApps); + updateSwappableAppsTask.execute(); + } else { + showJoinWifi(); + } + } + + /** + * Once the UpdateAsyncTask has finished preparing our repository index, we can + * show the next screen to the user. + */ + private void onLocalRepoPrepared() { + updateSwappableAppsTask = null; + hasPreparedLocalRepo = true; + showJoinWifi(); + } + + private void showJoinWifi() { + inflateInnerView(R.layout.swap_join_wifi); + } + + class UpdateAsyncTask extends AsyncTask { + + @SuppressWarnings("UnusedDeclaration") + private static final String TAG = "UpdateAsyncTask"; + + @NonNull + private final ProgressDialog progressDialog; + + @NonNull + private final Set selectedApps; + + @NonNull + private final Uri sharingUri; + + @NonNull + private final Context context; + + public UpdateAsyncTask(Context c, @NonNull Set apps) { + context = SwapWorkflowActivity.this.getApplicationContext(); + selectedApps = apps; + progressDialog = new ProgressDialog(c); + progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + progressDialog.setTitle(R.string.updating); + sharingUri = Utils.getSharingUri(FDroidApp.repo); + } + + @Override + protected void onPreExecute() { + progressDialog.show(); + } + + @Override + protected Void doInBackground(Void... params) { + try { + final LocalRepoManager lrm = LocalRepoManager.get(context); + publishProgress(getString(R.string.deleting_repo)); + lrm.deleteRepo(); + for (String app : selectedApps) { + publishProgress(String.format(getString(R.string.adding_apks_format), app)); + lrm.addApp(context, app); + } + lrm.writeIndexPage(sharingUri.toString()); + publishProgress(getString(R.string.writing_index_jar)); + lrm.writeIndexJar(); + publishProgress(getString(R.string.linking_apks)); + lrm.copyApksToRepo(); + publishProgress(getString(R.string.copying_icons)); + // run the icon copy without progress, its not a blocker + new AsyncTask() { + + @Override + protected Void doInBackground(Void... params) { + lrm.copyIconsToRepo(); + return null; + } + }.execute(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + @Override + protected void onProgressUpdate(String... progress) { + super.onProgressUpdate(progress); + progressDialog.setMessage(progress[0]); + } + + @Override + protected void onPostExecute(Void result) { + progressDialog.dismiss(); + Toast.makeText(context, R.string.updated_local_repo, Toast.LENGTH_SHORT).show(); + onLocalRepoPrepared(); + } + } + +} diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/views/SelectAppsView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/views/SelectAppsView.java new file mode 100644 index 000000000..7a17788da --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/views/SelectAppsView.java @@ -0,0 +1,302 @@ +package org.fdroid.fdroid.views.swap.views; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.view.MenuItemCompat; +import android.support.v4.widget.CursorAdapter; +import android.support.v7.widget.SearchView; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.InstalledAppProvider; +import org.fdroid.fdroid.localrepo.LocalRepoManager; +import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; + +import java.util.HashSet; + +public class SelectAppsView extends ListView implements + SwapWorkflowActivity.InnerView, + LoaderManager.LoaderCallbacks, + SearchView.OnQueryTextListener { + + public SelectAppsView(Context context) { + super(context); + } + + public SelectAppsView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SelectAppsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public SelectAppsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + private SwapWorkflowActivity getActivity() { + return (SwapWorkflowActivity)getContext(); + } + + private static final int LOADER_INSTALLED_APPS = 253341534; + + private AppListAdapter adapter; + private String mCurrentFilterString; + private final Presenter presenter = new Presenter(); + + public static class Presenter { + + private SelectAppsView view; + + public void setView(@NonNull SelectAppsView view) { + this.view = view; + } + + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + adapter = new AppListAdapter(this, getContext(), + getContext().getContentResolver().query(InstalledAppProvider.getContentUri(), InstalledAppProvider.DataColumns.ALL, null, null, null)); + setAdapter(adapter); + addHeaderView(inflate(getContext(), R.layout.swap_create_header, null), null, false); + setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + + // either reconnect with an existing loader or start a new one + getActivity().getSupportLoaderManager().initLoader(LOADER_INSTALLED_APPS, null, this); + + // build list of existing apps from what is on the file system + if (FDroidApp.selectedApps == null) { + FDroidApp.selectedApps = new HashSet<>(); + for (String filename : LocalRepoManager.get(getActivity()).repoDir.list()) { + if (filename.matches(".*\\.apk")) { + String packageName = filename.substring(0, filename.indexOf("_")); + FDroidApp.selectedApps.add(packageName); + } + } + } + + setOnItemClickListener(new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView parent, View v, int position, long id) { + if (position > 0) { + // Ignore the headerView at position 0. + toggleAppSelected(position); + } + } + }); + + presenter.setView(this); + } + + @Override + public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) { + + inflater.inflate(R.menu.swap_next_search, menu); + MenuItem nextMenuItem = menu.findItem(R.id.action_next); + int flags = MenuItemCompat.SHOW_AS_ACTION_ALWAYS | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT; + MenuItemCompat.setShowAsAction(nextMenuItem, flags); + nextMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + getActivity().onAppsSelected(FDroidApp.selectedApps); + return true; + } + }); + + 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); + return true; + } + + private void toggleAppSelected(int position) { + Cursor c = (Cursor) adapter.getItem(position - 1); + String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)); + if (FDroidApp.selectedApps.contains(packageName)) { + FDroidApp.selectedApps.remove(packageName); + } else { + FDroidApp.selectedApps.add(packageName); + } + } + + @Override + public CursorLoader onCreateLoader(int id, Bundle args) { + Uri uri; + if (TextUtils.isEmpty(mCurrentFilterString)) { + uri = InstalledAppProvider.getContentUri(); + } else { + uri = InstalledAppProvider.getSearchUri(mCurrentFilterString); + } + return new CursorLoader( + getActivity(), + uri, + InstalledAppProvider.DataColumns.ALL, + null, + null, + InstalledAppProvider.DataColumns.APPLICATION_LABEL); + } + + @Override + public void onLoadFinished(Loader loader, Cursor cursor) { + adapter.swapCursor(cursor); + + String fdroid = loader.getContext().getPackageName(); + for (int i = 0; i < getCount() - 1; i++) { + Cursor c = ((Cursor) getItemAtPosition(i + 1)); + String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)); + if (TextUtils.equals(packageName, fdroid)) { + setItemChecked(i + 1, true); // always include FDroid + } else { + for (String selected : FDroidApp.selectedApps) { + if (TextUtils.equals(packageName, selected)) { + setItemChecked(i + 1, true); + } + } + } + } + } + + @Override + public void onLoaderReset(Loader loader) { + adapter.swapCursor(null); + } + + @Override + public boolean onQueryTextChange(String newText) { + String newFilter = !TextUtils.isEmpty(newText) ? newText : null; + if (mCurrentFilterString == null && newFilter == null) { + return true; + } + if (mCurrentFilterString != null && mCurrentFilterString.equals(newFilter)) { + return true; + } + mCurrentFilterString = newFilter; + getActivity().getSupportLoaderManager().restartLoader(LOADER_INSTALLED_APPS, null, this); + return true; + } + + @Override + public boolean onQueryTextSubmit(String query) { + // this is not needed since we respond to every change in text + return true; + } + + private class AppListAdapter extends CursorAdapter { + + @SuppressWarnings("UnusedDeclaration") + private static final String TAG = "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; + } + + 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, parent, false); + 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 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(listPosition); + } + }); + } + } + } + +} diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/views/StartSwapView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/views/StartSwapView.java index 97a5e7095..72560f92d 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/views/StartSwapView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/views/StartSwapView.java @@ -3,18 +3,18 @@ package org.fdroid.fdroid.views.swap.views; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; +import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.ContextThemeWrapper; +import android.view.Menu; +import android.view.MenuInflater; import android.view.View; import android.widget.LinearLayout; -import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.views.swap.SwapActivity; +import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; -import java.util.Set; - -public class StartSwapView extends LinearLayout { +public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.InnerView { public StartSwapView(Context context) { super(context); @@ -36,9 +36,9 @@ public class StartSwapView extends LinearLayout { - private SwapActivity getActivity() { + private SwapWorkflowActivity getActivity() { // TODO: Try and find a better way to get to the SwapActivity, which makes less asumptions. - return (SwapActivity)((ContextThemeWrapper)getContext()).getBaseContext(); + return (SwapWorkflowActivity)getContext(); } @Override @@ -54,4 +54,8 @@ public class StartSwapView extends LinearLayout { } + @Override + public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) { + return false; + } }