diff --git a/android/phoenix/.classpath b/android/phoenix/.classpath index 51769745b2..08152dcbe2 100644 --- a/android/phoenix/.classpath +++ b/android/phoenix/.classpath @@ -5,5 +5,6 @@ + diff --git a/android/phoenix/libs/jsoup-1.8.1.jar b/android/phoenix/libs/jsoup-1.8.1.jar new file mode 100644 index 0000000000..ae717d450e Binary files /dev/null and b/android/phoenix/libs/jsoup-1.8.1.jar differ diff --git a/android/phoenix/res/values/strings.xml b/android/phoenix/res/values/strings.xml index f8907d16c2..e78f00f710 100644 --- a/android/phoenix/res/values/strings.xml +++ b/android/phoenix/res/values/strings.xml @@ -40,6 +40,11 @@ Manufacturer Permissions + + Confirm + Are you sure you want to download %1$s? + Downloading %1$s… + Refresh rate calibration Touch the screen with your fingers for more accurate measurements. diff --git a/android/phoenix/src/com/retroarch/browser/coremanager/CoreManagerActivity.java b/android/phoenix/src/com/retroarch/browser/coremanager/CoreManagerActivity.java index 9bb1a5e33a..efb2321714 100644 --- a/android/phoenix/src/com/retroarch/browser/coremanager/CoreManagerActivity.java +++ b/android/phoenix/src/com/retroarch/browser/coremanager/CoreManagerActivity.java @@ -4,6 +4,7 @@ import java.util.List; import com.retroarch.R; import com.retroarch.browser.coremanager.fragments.DownloadableCoresFragment; +import com.retroarch.browser.coremanager.fragments.InstalledCoresFragment; import com.retroarch.browser.coremanager.fragments.InstalledCoresManagerFragment; import android.os.Bundle; @@ -17,7 +18,7 @@ import android.support.v7.app.ActionBarActivity; * Activity which provides the base for viewing installed cores, * as well as the ability to download other cores. */ -public final class CoreManagerActivity extends ActionBarActivity +public final class CoreManagerActivity extends ActionBarActivity implements DownloadableCoresFragment.OnCoreDownloadedListener { @Override public void onCreate(Bundle savedInstanceState) @@ -76,6 +77,19 @@ public final class CoreManagerActivity extends ActionBarActivity return false; } + // Callback function used to update the installed cores list + @Override + public void onCoreDownloaded() + { + InstalledCoresManagerFragment icmf = (InstalledCoresManagerFragment) getSupportFragmentManager().findFragmentByTag("android:switcher:" + R.id.coreviewer_viewPager + ":" + 0); + if (icmf != null) + { + InstalledCoresFragment icf = (InstalledCoresFragment) icmf.getChildFragmentManager().findFragmentByTag("InstalledCoresList"); + if (icf != null) + icf.updateInstalledCoresList(); + } + } + // Adapter for the core manager ViewPager. private final class ViewPagerAdapter extends FragmentPagerAdapter { diff --git a/android/phoenix/src/com/retroarch/browser/coremanager/fragments/DownloadableCore.java b/android/phoenix/src/com/retroarch/browser/coremanager/fragments/DownloadableCore.java new file mode 100644 index 0000000000..9e55b96ccb --- /dev/null +++ b/android/phoenix/src/com/retroarch/browser/coremanager/fragments/DownloadableCore.java @@ -0,0 +1,57 @@ +package com.retroarch.browser.coremanager.fragments; + +/** + * Represents a core that can be downloaded. + */ +final class DownloadableCore +{ + private final String coreName; + private final String coreURL; + private final String shortURL; + + /** + * Constructor + * + * @param coreName Name of the core. + * @param coreURL URL to this core. + */ + public DownloadableCore(String coreName, String coreURL) + { + this.coreName = coreName; + this.coreURL = coreURL; + this.shortURL = coreURL.substring(coreURL.lastIndexOf('/') + 1); + } + + /** + * Gets the name of this core. + * + * @return The name of this core. + */ + public String getCoreName() + { + return coreName; + } + + /** + * Gets the URL to download this core. + * + * @return The URL to download this core. + */ + public String getCoreURL() + { + return coreURL; + } + + /** + * Gets the short URL name of this core. + *

+ * e.g. Consider the url: www.somesite/somecore.zip. + * This would return "somecore.zip" + * + * @return the short URL name of this core. + */ + public String getShortURLName() + { + return shortURL; + } +} diff --git a/android/phoenix/src/com/retroarch/browser/coremanager/fragments/DownloadableCoresAdapter.java b/android/phoenix/src/com/retroarch/browser/coremanager/fragments/DownloadableCoresAdapter.java new file mode 100644 index 0000000000..63e63f5158 --- /dev/null +++ b/android/phoenix/src/com/retroarch/browser/coremanager/fragments/DownloadableCoresAdapter.java @@ -0,0 +1,54 @@ +package com.retroarch.browser.coremanager.fragments; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +/** + * {@link ArrayAdapter} that handles the display of downloadable cores. + */ +final class DownloadableCoresAdapter extends ArrayAdapter +{ + private final int layoutId; + + /** + * Constructor + * + * @param context The current {@link Context}. + * @param layoutID The resource ID for a layout file containing a layout to use when instantiating views + */ + public DownloadableCoresAdapter(Context context, int layoutId) + { + super(context, layoutId); + + this.layoutId = layoutId; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) + { + if (convertView == null) + { + final LayoutInflater li = LayoutInflater.from(getContext()); + convertView = li.inflate(layoutId, parent, false); + } + + final DownloadableCore core = getItem(position); + if (core != null) + { + TextView title = (TextView) convertView.findViewById(android.R.id.text1); + TextView subtitle = (TextView) convertView.findViewById(android.R.id.text2); + + if (title != null) + title.setText(core.getCoreName()); + + if (subtitle != null) + subtitle.setText(core.getCoreURL()); + } + + return convertView; + } +} \ No newline at end of file diff --git a/android/phoenix/src/com/retroarch/browser/coremanager/fragments/DownloadableCoresFragment.java b/android/phoenix/src/com/retroarch/browser/coremanager/fragments/DownloadableCoresFragment.java index 19fd4cdf46..9b91117ce7 100644 --- a/android/phoenix/src/com/retroarch/browser/coremanager/fragments/DownloadableCoresFragment.java +++ b/android/phoenix/src/com/retroarch/browser/coremanager/fragments/DownloadableCoresFragment.java @@ -1,27 +1,378 @@ package com.retroarch.browser.coremanager.fragments; +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.jsoup.Connection; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.AsyncTask; import android.os.Bundle; -import android.support.v4.app.Fragment; import android.support.v4.app.ListFragment; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; +import android.widget.ListView; + +import com.retroarch.R; /** * {@link ListFragment} that is responsible for showing * cores that are able to be downloaded or are not installed. */ -public final class DownloadableCoresFragment extends Fragment +public final class DownloadableCoresFragment extends ListFragment { - // TODO: Implement complete functionality. + // List of TODOs. + // - Eventually make the core downloader capable of directory-based browsing from the base URL. + // - Allow for 'repository'-like core downloading. + // - Clean this up a little better. It can likely be way more organized. + // - Don't re-download the info files on orientation changes. + // - Use a loading wheel when core retrieval is being done. User may think something went wrong otherwise. + // - Check the info directory for an info file before downloading it. Can save bandwidth this way (and list load times would be faster). + // - Should probably display a dialog or a toast message when the Internet connection process fails. + + /** + * Dictates what actions will occur when a core download completes. + *

+ * Acts like a callback so that communication between fragments is possible. + */ + public interface OnCoreDownloadedListener + { + /** The action that will occur when a core is successfully downloaded. */ + void onCoreDownloaded(); + } + + private static final String BUILDBOT_BASE_URL = "http://buildbot.libretro.com"; + private static final String BUILDBOT_CORE_URL = BUILDBOT_BASE_URL + "/stable/android/armv7/1.0.0.2/cores/"; + private static final String BUILDBOT_INFO_URL = BUILDBOT_BASE_URL + "/stable/android/armv7/1.0.0.2/info/"; + + private OnCoreDownloadedListener coreDownloadedListener = null; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - TextView tv = new TextView(getActivity()); - tv.setText("In development, just a little longer!"); + super.onCreateView(inflater, container, savedInstanceState); + final ListView coreList = (ListView) inflater.inflate(R.layout.coremanager_listview, container, false); - return tv; + final DownloadableCoresAdapter adapter = new DownloadableCoresAdapter(getActivity(), android.R.layout.simple_list_item_2); + adapter.setNotifyOnChange(true); + coreList.setAdapter(adapter); + + coreDownloadedListener = (OnCoreDownloadedListener) getActivity(); + + new PopulateCoresListOperation(adapter).execute(); + + return coreList; + } + + @Override + public void onListItemClick(final ListView lv, final View v, final int position, final long id) + { + super.onListItemClick(lv, v, position, id); + final DownloadableCore core = (DownloadableCore) lv.getItemAtPosition(position); + + // Prompt the user for confirmation on downloading the core. + AlertDialog.Builder notification = new AlertDialog.Builder(getActivity()); + notification.setMessage(String.format(getString(R.string.download_core_confirm_msg), core.getCoreName())); + notification.setTitle(R.string.download_core_confirm_title); + notification.setNegativeButton(R.string.no, null); + notification.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) + { + // Begin downloading the core. + new DownloadCoreOperation(getActivity(), core.getCoreName()).execute(core.getCoreURL(), core.getShortURLName()); + } + }); + notification.show(); + } + + // Async event responsible for populating the Downloadable Cores list. + private static final class PopulateCoresListOperation extends AsyncTask> + { + // Acts as an object reference to an adapter in a list view. + private DownloadableCoresAdapter adapter; + + /** + * Constructor + * + * @param adapter The adapter to asynchronously update. + */ + public PopulateCoresListOperation(DownloadableCoresAdapter adapter) + { + this.adapter = adapter; + } + + @Override + protected ArrayList doInBackground(Void... params) + { + try + { + final Connection core_connection = Jsoup.connect(BUILDBOT_CORE_URL); + final Elements coreElements = core_connection.get().body().getElementsByClass("fb-n").select("a"); + + final ArrayList downloadableCores = new ArrayList(); + + // NOTE: Start from 1 to skip the ".." (parent directory element) + // Set this to zero if directory-based browsing becomes a thing. + for (int i = 1; i < coreElements.size(); i++) + { + Element coreElement = coreElements.get(i); + + final String coreURL = BUILDBOT_BASE_URL + coreElement.attr("href"); + final String coreName = coreURL.substring(coreURL.lastIndexOf("/") + 1); + final String infoURL = BUILDBOT_INFO_URL + coreName.replace("_android.so.zip", ".info"); + + downloadableCores.add(new DownloadableCore(getCoreName(infoURL), coreURL)); + } + + return downloadableCores; + } + catch (IOException e) + { + Log.e("PopulateCoresListOperation", e.getMessage()); + + // Make a dummy entry to notify an error. + final ArrayList errorList = new ArrayList(); + errorList.add(new DownloadableCore("Error", e.getMessage())); + return errorList; + } + } + + @Override + protected void onPostExecute(ArrayList result) + { + super.onPostExecute(result); + adapter.addAll(result); + } + + // Literally downloads the info file, writes it, and parses it for the corename key/value pair. + // AKA an argument for having a manifest file on the server. + // + // This makes list loading take way longer than it should. + // + // One way this can be improved is by checking the info directory for + // existing info files that match the core. Eliminating the download retrieval. + private String getCoreName(String urlPath) throws IOException + { + final URL url = new URL(urlPath); + final BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream())); + final StringBuilder sb = new StringBuilder(); + + String str = ""; + while ((str = br.readLine()) != null) + sb.append(str + "\n"); + br.close(); + + // Write the info file. + File outputPath = new File(adapter.getContext().getApplicationInfo().dataDir + "/info/", urlPath.substring(urlPath.lastIndexOf('/') + 1)); + BufferedWriter bw = new BufferedWriter(new FileWriter(outputPath)); + bw.append(sb); + bw.close(); + + // Now read the core name + String[] lines = sb.toString().split("\n"); + String name = ""; + for (int i = 0; i < lines.length; i++) + { + if (lines[i].contains("corename")) + { + // Gross + name = lines[i].split("=")[1].trim().replace("\"", ""); + break; + } + } + + return name; + } + } + + // Executed when the user confirms a core download. + private final class DownloadCoreOperation extends AsyncTask + { + private final ProgressDialog dlg; + private final Context ctx; + private final String coreName; + + /** + * Constructor + * + * @param ctx The current {@link Context}. + * @param coreName The name of the core being downloaded. + */ + public DownloadCoreOperation(Context ctx, String coreName) + { + this.dlg = new ProgressDialog(ctx); + this.ctx = ctx; + this.coreName = coreName; + } + + @Override + protected void onPreExecute() + { + super.onPreExecute(); + + dlg.setMessage(String.format(ctx.getString(R.string.downloading_msg), coreName)); + dlg.setCancelable(false); + dlg.setCanceledOnTouchOutside(false); + dlg.setIndeterminate(false); + dlg.setMax(100); + dlg.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + dlg.show(); + } + + @Override + protected Void doInBackground(String... params) + { + InputStream input = null; + OutputStream output = null; + HttpURLConnection connection = null; + try + { + URL url = new URL(params[0]); + connection = (HttpURLConnection) url.openConnection(); + connection.connect(); + + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) + { + Log.i("DownloadCoreOperation", "HTTP response code not OK. Response code: " + connection.getResponseCode()); + return null; + } + + // Set up the streams + final int fileLen = connection.getContentLength(); + final File zipPath = new File(ctx.getApplicationInfo().dataDir + "/cores/", params[1]); + input = new BufferedInputStream(connection.getInputStream(), 8192); + output = new FileOutputStream(zipPath); + + // Download and write to storage. + long totalDownloaded = 0; + byte[] buffer = new byte[4096]; + int countBytes = 0; + while ((countBytes = input.read(buffer)) != -1) + { + totalDownloaded += countBytes; + if (fileLen > 0) + publishProgress((int) (totalDownloaded * 100 / fileLen)); + + output.write(buffer, 0, countBytes); + } + + unzipCore(zipPath); + } + catch (IOException ignored) + { + // Can't really do anything to recover. + } + finally + { + try + { + if (output != null) + output.close(); + + if (input != null) + input.close(); + } + catch (IOException ignored) + { + } + + if (connection != null) + connection.disconnect(); + } + + return null; + } + + @Override + protected void onProgressUpdate(Integer... progress) + { + super.onProgressUpdate(progress); + + dlg.setProgress(progress[0]); + } + + @Override + protected void onPostExecute(Void result) + { + super.onPostExecute(result); + + if (dlg.isShowing()) + dlg.dismiss(); + + // Invoke callback to update the installed cores list. + coreDownloadedListener.onCoreDownloaded(); + } + } + + // Java 6 ladies and gentlemen. + private static void unzipCore(File zipFile) + { + ZipInputStream zis = null; + + try + { + zis = new ZipInputStream(new FileInputStream(zipFile)); + ZipEntry entry = zis.getNextEntry(); + + while (entry != null) + { + File file = new File(zipFile.getParent(), entry.getName()); + + FileOutputStream fos = new FileOutputStream(file); + int len = 0; + byte[] buffer = new byte[4096]; + while ((len = zis.read(buffer)) != -1) + { + fos.write(buffer, 0, len); + } + fos.close(); + + entry = zis.getNextEntry(); + } + } + catch (IOException ignored) + { + // Can't do anything. + } + finally + { + try + { + if (zis != null) + { + zis.closeEntry(); + zis.close(); + } + } + catch (IOException ignored) + { + // Can't do anything + } + + zipFile.delete(); + } } } diff --git a/android/phoenix/src/com/retroarch/browser/coremanager/fragments/InstalledCoresFragment.java b/android/phoenix/src/com/retroarch/browser/coremanager/fragments/InstalledCoresFragment.java index 75c9d79fbc..1773fff3fb 100644 --- a/android/phoenix/src/com/retroarch/browser/coremanager/fragments/InstalledCoresFragment.java +++ b/android/phoenix/src/com/retroarch/browser/coremanager/fragments/InstalledCoresFragment.java @@ -56,70 +56,23 @@ public final class InstalledCoresFragment extends ListFragment } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + public void onActivityCreated(Bundle savedInstanceState) { - // Inflate the layout for this ListFragment. - ListView parentView = (ListView) inflater.inflate(R.layout.coremanager_listview, container, false); + super.onActivityCreated(savedInstanceState); - // Set the long click listener. - parentView.setOnItemLongClickListener(itemLongClickListener); + adapter = new InstalledCoresAdapter(getActivity(), android.R.layout.simple_list_item_2, getInstalledCoresList()); + setListAdapter(adapter); // Get the callback. (implemented within InstalledCoresManagerFragment). callback = (OnCoreItemClickedListener) getParentFragment(); + } - // The list of items that will be added to the adapter backing this ListFragment. - final List items = new ArrayList(); + @Override + public void onViewCreated(View view, Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); - // Check if the device supports NEON. - final String cpuInfo = UserPreferences.readCPUInfo(); - final boolean supportsNeon = cpuInfo.contains("neon"); - - // Populate the list - final File[] libs = new File(getActivity().getApplicationInfo().dataDir, "/cores").listFiles(); - if (libs != null) - { - for (File lib : libs) - { - String libName = lib.getName(); - - // Never append a NEON lib if we don't have NEON. - if (libName.contains("neon") && !supportsNeon) - continue; - - // If we have a NEON version with NEON capable CPU, - // never append a non-NEON version. - if (supportsNeon && !libName.contains("neon")) - { - boolean hasNeonVersion = false; - for (File lib_ : libs) - { - String otherName = lib_.getName(); - String baseName = libName.replace(".so", ""); - - if (otherName.contains("neon") && otherName.startsWith(baseName)) - { - hasNeonVersion = true; - break; - } - } - - if (hasNeonVersion) - continue; - } - - // Add it to the list. - items.add(new ModuleWrapper(getActivity(), lib)); - } - } - - // Sort the list alphabetically - Collections.sort(items); - - // Initialize and set the backing adapter for this ListFragment. - adapter = new InstalledCoresAdapter(getActivity(), android.R.layout.simple_list_item_2, items); - parentView.setAdapter(adapter); - - return parentView; + getListView().setOnItemLongClickListener(itemLongClickListener); } @Override @@ -131,6 +84,68 @@ public final class InstalledCoresFragment extends ListFragment getListView().setItemChecked(position, true); } + /** + * Refreshes the list of installed cores. + */ + public void updateInstalledCoresList() + { + adapter.clear(); + adapter.addAll(getInstalledCoresList()); + adapter.notifyDataSetChanged(); + } + + private List getInstalledCoresList() + { + // The list of items that will be added to the adapter backing this ListFragment. + final List items = new ArrayList(); + + // Check if the device supports NEON. + final String cpuInfo = UserPreferences.readCPUInfo(); + final boolean supportsNeon = cpuInfo.contains("neon"); + + // Populate the list + final File[] libs = new File(getActivity().getApplicationInfo().dataDir, "/cores").listFiles(); + if (libs != null) + { + for (File lib : libs) + { + String libName = lib.getName(); + + // Never append a NEON lib if we don't have NEON. + if (libName.contains("neon") && !supportsNeon) + continue; + + // If we have a NEON version with NEON capable CPU, + // never append a non-NEON version. + if (supportsNeon && !libName.contains("neon")) + { + boolean hasNeonVersion = false; + for (File lib_ : libs) + { + String otherName = lib_.getName(); + String baseName = libName.replace(".so", ""); + + if (otherName.contains("neon") && otherName.startsWith(baseName)) + { + hasNeonVersion = true; + break; + } + } + + if (hasNeonVersion) + continue; + } + + // Add it to the list. + items.add(new ModuleWrapper(getActivity(), lib)); + } + } + + // Sort the list alphabetically + Collections.sort(items); + return items; + } + // This will be the handler for long clicks on individual list items in this ListFragment. private final OnItemLongClickListener itemLongClickListener = new OnItemLongClickListener() { diff --git a/android/phoenix/src/com/retroarch/browser/coremanager/fragments/InstalledCoresManagerFragment.java b/android/phoenix/src/com/retroarch/browser/coremanager/fragments/InstalledCoresManagerFragment.java index 9d2a6a9947..1178710130 100644 --- a/android/phoenix/src/com/retroarch/browser/coremanager/fragments/InstalledCoresManagerFragment.java +++ b/android/phoenix/src/com/retroarch/browser/coremanager/fragments/InstalledCoresManagerFragment.java @@ -23,7 +23,7 @@ public class InstalledCoresManagerFragment extends Fragment implements Installed final Fragment installedCores = new InstalledCoresFragment(); final FragmentTransaction ft = getChildFragmentManager().beginTransaction(); - ft.replace(R.id.installed_cores_fragment_container1, installedCores); + ft.replace(R.id.installed_cores_fragment_container1, installedCores, "InstalledCoresList"); ft.commit(); return v; diff --git a/android/phoenix/src/com/retroarch/browser/mainmenu/MainMenuActivity.java b/android/phoenix/src/com/retroarch/browser/mainmenu/MainMenuActivity.java index 9d543eaf66..603af52583 100644 --- a/android/phoenix/src/com/retroarch/browser/mainmenu/MainMenuActivity.java +++ b/android/phoenix/src/com/retroarch/browser/mainmenu/MainMenuActivity.java @@ -1,9 +1,12 @@ package com.retroarch.browser.mainmenu; +import java.io.File; + import com.retroarch.R; import com.retroarch.browser.preferences.util.UserPreferences; import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; import android.media.AudioManager; import android.os.Bundle; import android.preference.PreferenceActivity; @@ -21,6 +24,15 @@ public final class MainMenuActivity extends FragmentActivity { super.onCreate(savedInstanceState); + // Ensure resource directories are created. + final ApplicationInfo info = getApplicationInfo(); + final File coresDir = new File(info.dataDir, "cores"); + final File infoDir = new File(info.dataDir, "info"); + if (!coresDir.exists()) + coresDir.mkdir(); + if (!infoDir.exists()) + infoDir.mkdir(); + // Load the main menu layout setContentView(R.layout.mainmenu_activity_layout); if (savedInstanceState == null)