Kaynağa Gözat

change keyevent

wangyongj 2 yıl önce
ebeveyn
işleme
e11788ab12

+ 4 - 1
app/src/main/AndroidManifest.xml

@@ -15,10 +15,12 @@
         android:label="@string/app_name"
         android:roundIcon="@mipmap/ic_launcher_round"
         android:supportsRtl="true"
+        android:isGame="true"
+        android:hasCode="true"
         android:theme="@style/Theme.GameConsole"
         tools:targetApi="31">
         <activity
-            android:name=".MainActivity"
+            android:name=".TextCopyFileActivity"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -26,6 +28,7 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <activity android:name=".MainActivity"></activity>
         <activity
             android:name=".emulator.RetroArchEmulatorActivity"
             android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"

+ 615 - 0
app/src/main/java/com/retroarch/browser/retroactivity/RetroActivityCommon.java

@@ -0,0 +1,615 @@
+package com.retroarch.browser.retroactivity;
+
+import com.xugame.BuildConfig;
+import com.xugame.gameconsole.playcore.PlayCoreManager;
+import com.xugame.gameconsole.preferences.UserPreferences;
+import com.xugame.gameconsole.util.DebugUtil;
+
+import android.annotation.TargetApi;
+import android.app.NativeActivity;
+import android.content.res.Configuration;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.media.AudioAttributes;
+import android.os.Bundle;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+import android.system.Os;
+import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
+import android.view.Surface;
+import android.view.WindowManager;
+import android.app.UiModeManager;
+import android.os.BatteryManager;
+import android.os.Build;
+import android.os.PowerManager;
+import android.os.Vibrator;
+import android.os.VibrationEffect;
+import android.util.Log;
+
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.Locale;
+
+/**
+ * Class which provides common methods for RetroActivity related classes.
+ */
+public class RetroActivityCommon extends NativeActivity
+{
+  private static final String TAG = "RetroActivityCommonTAG";
+
+  static {
+    System.loadLibrary("retroarch-activity");
+  }
+
+  public static int FRONTEND_POWERSTATE_NONE = 0;
+  public static int FRONTEND_POWERSTATE_NO_SOURCE = 1;
+  public static int FRONTEND_POWERSTATE_CHARGING = 2;
+  public static int FRONTEND_POWERSTATE_CHARGED = 3;
+  public static int FRONTEND_POWERSTATE_ON_POWER_SOURCE = 4;
+  public static int FRONTEND_ORIENTATION_0 = 0;
+  public static int FRONTEND_ORIENTATION_90 = 1;
+  public static int FRONTEND_ORIENTATION_180 = 2;
+  public static int FRONTEND_ORIENTATION_270 = 3;
+  public static int RETRO_RUMBLE_STRONG = 0;
+  public static int RETRO_RUMBLE_WEAK = 1;
+  public boolean sustainedPerformanceMode = true;
+  public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    cleanupSymlinks();
+    updateSymlinks();
+
+    PlayCoreManager.getInstance().onCreate(this);
+    super.onCreate(savedInstanceState);
+  }
+
+  @Override
+  protected void onDestroy() {
+    PlayCoreManager.getInstance().onDestroy();
+    super.onDestroy();
+  }
+
+  public void doVibrate(int id, int effect, int strength, int oneShot)
+  {
+    Vibrator vibrator = null;
+    int repeat = 0;
+    long[] pattern = {0, 16};
+    int[] strengths = {0, strength};
+
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+      if (id == -1)
+        vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE);
+      else
+      {
+        InputDevice dev = InputDevice.getDevice(id);
+
+        if (dev != null)
+          vibrator = dev.getVibrator();
+      }
+    }
+
+    if (vibrator == null)
+      return;
+
+    if (strength == 0) {
+      vibrator.cancel();
+      return;
+    }
+
+    if (oneShot > 0)
+      repeat = -1;
+    else
+      pattern[1] = 1000;
+
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+      if (id >= 0)
+        Log.i("RetroActivity", "Vibrate id " + id + ": strength " + strength);
+
+      vibrator.vibrate(VibrationEffect.createWaveform(pattern, strengths, repeat), new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_GAME).setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build());
+    }else{
+      vibrator.vibrate(pattern, repeat);
+    }
+  }
+
+  public void doHapticFeedback(int effect)
+  {
+    getWindow().getDecorView().performHapticFeedback(effect,
+        HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING | HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+    Log.i("RetroActivity", "Haptic Feedback effect " + effect);
+  }
+
+  // Exiting cleanly from NDK seems to be nearly impossible.
+  // Have to use exit(0) to avoid weird things happening, even with runOnUiThread() approaches.
+  // Use a separate JNI function to explicitly trigger the readback.
+  public void onRetroArchExit()
+  {
+    DebugUtil.i(TAG,"onRetroArchExit");
+      finish();
+  }
+
+  public int getVolumeCount()
+  {
+    int ret = 0;
+
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+      StorageManager storageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);
+      List<StorageVolume> storageVolumeList = storageManager.getStorageVolumes();
+
+      for (int i = 0; i < storageVolumeList.size(); i++) {
+        ret++;
+      }
+      Log.i("RetroActivity", "volume count: " + ret);
+    }
+
+    return (int)ret;
+  }
+
+  public String getVolumePath(String input)
+  {
+    String ret = "";
+
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+      int index = Integer.valueOf(input);
+      int j = 0;
+
+      StorageManager storageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);
+      List<StorageVolume> storageVolumeList = storageManager.getStorageVolumes();
+
+      for (int i = 0; i < storageVolumeList.size(); i++) {
+        if (i == j) {
+          ret = String.valueOf(storageVolumeList.get(index).getDirectory());
+        }
+      }
+      Log.i("RetroActivity", "volume path: " + ret);
+    }
+
+    return ret;
+  }
+
+// https://stackoverflow.com/questions/4553650/how-to-check-device-natural-default-orientation-on-android-i-e-get-landscape/4555528#4555528
+  public int getDeviceDefaultOrientation() {
+    WindowManager windowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
+    Configuration config = getResources().getConfiguration();
+    int rotation = windowManager.getDefaultDisplay().getRotation();
+
+    if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) &&
+        config.orientation == Configuration.ORIENTATION_LANDSCAPE)
+        || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) &&
+        config.orientation == Configuration.ORIENTATION_PORTRAIT))
+    {
+      return Configuration.ORIENTATION_LANDSCAPE;
+    }else{
+      return Configuration.ORIENTATION_PORTRAIT;
+    }
+  }
+
+  public void setScreenOrientation(int orientation)
+  {
+    int naturalOrientation = getDeviceDefaultOrientation();
+    int newOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
+    // We assume no device has a natural orientation that is reversed
+    switch (naturalOrientation) {
+      case Configuration.ORIENTATION_PORTRAIT:
+      {
+        if (orientation == FRONTEND_ORIENTATION_0) {
+          newOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+        }else if (orientation == FRONTEND_ORIENTATION_90) {
+          newOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+        }else if (orientation == FRONTEND_ORIENTATION_180) {
+          newOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+        }else if (orientation == FRONTEND_ORIENTATION_270) {
+          newOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+        }
+        break;
+      }
+      case Configuration.ORIENTATION_LANDSCAPE:
+      {
+        if (orientation == FRONTEND_ORIENTATION_0) {
+          newOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+        }else if (orientation == FRONTEND_ORIENTATION_90) {
+          newOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+        }else if (orientation == FRONTEND_ORIENTATION_180) {
+          newOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+        }else if (orientation == FRONTEND_ORIENTATION_270) {
+          newOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+        }
+        break;
+      }
+    }
+
+    screenOrientation = newOrientation;
+
+    Log.i("RetroActivity", "setting new orientation to " + screenOrientation);
+
+    runOnUiThread(new Runnable() {
+      @Override
+      public void run() {
+        setRequestedOrientation(screenOrientation);
+      }
+    });
+  }
+
+  public String getUserLanguageString()
+  {
+    String lang = Locale.getDefault().getLanguage();
+    String country = Locale.getDefault().getCountry();
+
+    if (lang.length() == 0)
+      return "en";
+
+    if (country.length() == 0)
+      return lang;
+
+    return lang + '_' + country;
+  }
+
+  @TargetApi(24)
+  public void setSustainedPerformanceMode(boolean on)
+  {
+    sustainedPerformanceMode = on;
+
+    if (Build.VERSION.SDK_INT >= 24) {
+      if (isSustainedPerformanceModeSupported()) {
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        runOnUiThread(new Runnable() {
+          @Override
+          public void run() {
+            Log.i("RetroActivity", "setting sustained performance mode to " + sustainedPerformanceMode);
+
+            getWindow().setSustainedPerformanceMode(sustainedPerformanceMode);
+
+            latch.countDown();
+          }
+        });
+
+        try {
+          latch.await();
+        }catch(InterruptedException e) {
+          e.printStackTrace();
+        }
+      }
+    }
+  }
+
+  @TargetApi(24)
+  public boolean isSustainedPerformanceModeSupported()
+  {
+    boolean supported = false;
+
+    if (Build.VERSION.SDK_INT >= 24)
+    {
+      PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
+
+      if (powerManager.isSustainedPerformanceModeSupported())
+        supported = true;
+    }
+
+    Log.i("RetroActivity", "isSustainedPerformanceModeSupported? " + supported);
+
+    return supported;
+  }
+
+  public int getBatteryLevel()
+  {
+    IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+    // This doesn't actually register anything (or need to) because we know this particular intent is sticky and we do not specify a BroadcastReceiver anyway
+    Intent batteryStatus = registerReceiver(null, ifilter);
+    int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
+    int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
+
+    float percent = ((float)level / (float)scale) * 100.0f;
+
+    Log.i("RetroActivity", "battery: level = " + level + ", scale = " + scale + ", percent = " + percent);
+
+    return (int)percent;
+  }
+
+  public int getPowerstate()
+  {
+    IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+    // This doesn't actually register anything (or need to) because we know this particular intent is sticky and we do not specify a BroadcastReceiver anyway
+    Intent batteryStatus = registerReceiver(null, ifilter);
+    int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
+    boolean hasBattery = batteryStatus.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false);
+    boolean isCharging = (status == BatteryManager.BATTERY_STATUS_CHARGING);
+    boolean isCharged = (status == BatteryManager.BATTERY_STATUS_FULL);
+    int powerstate = FRONTEND_POWERSTATE_NONE;
+
+    if (isCharged)
+      powerstate = FRONTEND_POWERSTATE_CHARGED;
+    else if (isCharging)
+      powerstate = FRONTEND_POWERSTATE_CHARGING;
+    else if (!hasBattery)
+      powerstate = FRONTEND_POWERSTATE_NO_SOURCE;
+    else
+      powerstate = FRONTEND_POWERSTATE_ON_POWER_SOURCE;
+
+    Log.i("RetroActivity", "power state = " + powerstate);
+
+    return powerstate;
+  }
+
+  public boolean isAndroidTV()
+  {
+    Configuration config = getResources().getConfiguration();
+    UiModeManager uiModeManager = (UiModeManager)getSystemService(UI_MODE_SERVICE);
+
+    if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
+    {
+      Log.i("RetroActivity", "isAndroidTV == true");
+      return true;
+    }
+    else
+    {
+      Log.i("RetroActivity", "isAndroidTV == false");
+      return false;
+    }
+  }
+
+  @Override
+  public void onConfigurationChanged(Configuration newConfig) {
+    int oldOrientation = 0;
+    boolean hasOldOrientation = false;
+
+    super.onConfigurationChanged(newConfig);
+
+    Log.i("RetroActivity", "onConfigurationChanged: orientation is now " + newConfig.orientation);
+
+    SharedPreferences prefs = UserPreferences.getPreferences(this);
+    SharedPreferences.Editor edit = prefs.edit();
+
+    hasOldOrientation = prefs.contains("ORIENTATION");
+
+    if (hasOldOrientation)
+      oldOrientation = prefs.getInt("ORIENTATION", 0);
+
+    edit.putInt("ORIENTATION", newConfig.orientation);
+    edit.apply();
+
+    Log.i("RetroActivity", "hasOldOrientation? " + hasOldOrientation + " newOrientation: " + newConfig.orientation + " oldOrientation: " + oldOrientation);
+  }
+
+  /**
+   * Checks if this version of RetroArch is a Play Store build.
+   *
+   * @return true if this is a Play Store build, false otherwise
+   */
+  public boolean isPlayStoreBuild() {
+    Log.i("RetroActivity", "isPlayStoreBuild: " + BuildConfig.PLAY_STORE_BUILD);
+
+    return BuildConfig.PLAY_STORE_BUILD;
+  }
+
+  /**
+   * Gets the list of available cores that can be downloaded as Dynamic Feature Modules.
+   *
+   * @return the list of available cores
+   */
+  public String[] getAvailableCores() {
+    int id = getResources().getIdentifier("module_names_" + Build.CPU_ABI.replace('-', '_'), "array", getPackageName());
+
+    String[] returnVal = getResources().getStringArray(id);
+    Log.i("RetroActivity", "getAvailableCores: " + Arrays.toString(returnVal));
+    return returnVal;
+  }
+
+  /**
+   * Gets the list of cores that are currently installed as Dynamic Feature Modules.
+   *
+   * @return the list of installed cores
+   */
+  public String[] getInstalledCores() {
+    String[] modules = PlayCoreManager.getInstance().getInstalledModules();
+    List<String> cores = new ArrayList<>();
+    List<String> availableCores = Arrays.asList(getAvailableCores());
+
+    SharedPreferences prefs = UserPreferences.getPreferences(this);
+
+    for(int i = 0; i < modules.length; i++) {
+      String coreName = unsanitizeCoreName(modules[i]);
+      if(!prefs.getBoolean("core_deleted_" + coreName, false)
+              && availableCores.contains(coreName)) {
+        cores.add(coreName);
+      }
+    }
+
+    String[] returnVal = cores.toArray(new String[0]);
+    Log.i("RetroActivity", "getInstalledCores: " + Arrays.toString(returnVal));
+    return returnVal;
+  }
+
+  /**
+   * Asks the system to download a core.
+   *
+   * @param coreName Name of the core to install
+   */
+  public void downloadCore(final String coreName) {
+    Log.i("RetroActivity", "downloadCore: " + coreName);
+
+    SharedPreferences prefs = UserPreferences.getPreferences(this);
+    prefs.edit().remove("core_deleted_" + coreName).apply();
+
+    PlayCoreManager.getInstance().downloadCore(coreName);
+  }
+
+  /**
+   * Asks the system to delete a core.
+   *
+   * Note that the actual module deletion will not happen immediately (the OS will delete
+   * it whenever it feels like it), but the symlink will still be immediately removed.
+   *
+   * @param coreName Name of the core to delete
+   */
+  public void deleteCore(String coreName) {
+    Log.i("RetroActivity", "deleteCore: " + coreName);
+
+    String newFilename = getCorePath() + coreName + "_libretro_android.so";
+    new File(newFilename).delete();
+
+    SharedPreferences prefs = UserPreferences.getPreferences(this);
+    prefs.edit().putBoolean("core_deleted_" + coreName, true).apply();
+
+    PlayCoreManager.getInstance().deleteCore(coreName);
+  }
+
+
+
+  /////////////// JNI methods ///////////////
+public native void clickBack();
+
+
+
+
+  /**
+   * Called when a core install is initiated.
+   *
+   * @param coreName Name of the core that the install is initiated for.
+   * @param successful true if success, false if failure
+   */
+  public native void coreInstallInitiated(String coreName, boolean successful);
+
+  /**
+   * Called when the status of a core install has changed.
+   *
+   * @param coreNames Names of all cores that are currently being downloaded.
+   * @param status One of INSTALL_STATUS_DOWNLOADING, INSTALL_STATUS_INSTALLING,
+   *               INSTALL_STATUS_INSTALLED, or INSTALL_STATUS_FAILED
+   * @param bytesDownloaded Number of bytes downloaded.
+   * @param totalBytesToDownload Total number of bytes to download.
+   */
+  public native void coreInstallStatusChanged(String[] coreNames, int status, long bytesDownloaded, long totalBytesToDownload);
+
+
+
+  /////////////// Private methods ///////////////
+
+
+
+  /**
+   * Sanitizes a core name so that it can be used when dealing with
+   * Dynamic Feature Modules. Needed because Gradle modules cannot use
+   * dashes, but we have at least one core name ("mesen-s") that uses them.
+   *
+   * @param coreName Name of the core to sanitize.
+   * @return The sanitized core name.
+   */
+  public String sanitizeCoreName(String coreName) {
+    return "core_" + coreName.replace('-', '_');
+  }
+
+  /**
+   * Unsanitizes a core name from its module name.
+   *
+   * @param coreName Name of the core to unsanitize.
+   * @return The unsanitized core name.
+   */
+  public String unsanitizeCoreName(String coreName) {
+    if(coreName.equals("core_mesen_s")) {
+      return "mesen-s";
+    }
+
+    return coreName.substring(5);
+  }
+
+  /**
+   * Gets the path to the RetroArch cores directory.
+   *
+   * @return The path to the RetroArch cores directory
+   */
+  private String getCorePath() {
+    String path = getApplicationInfo().dataDir + "/cores/";
+    new File(path).mkdirs();
+
+    return path;
+  }
+
+  /**
+   * Cleans up existing symlinks before new ones are created.
+   */
+  private void cleanupSymlinks() {
+    if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
+
+    File[] files = new File(getCorePath()).listFiles();
+    for(int i = 0; i < files.length; i++) {
+      try {
+        Os.readlink(files[i].getAbsolutePath());
+        files[i].delete();
+      } catch (Exception e) {
+        // File is not a symlink, so don't delete.
+      }
+    }
+  }
+
+  /**
+   * Triggers a symlink update in the known places that Dynamic Feature Modules
+   * are installed to.
+   */
+  public void updateSymlinks() {
+    if(!isPlayStoreBuild()) return;
+
+    traverseFilesystem(getFilesDir());
+    traverseFilesystem(new File(getApplicationInfo().nativeLibraryDir));
+  }
+
+  /**
+   * Traverse the filesystem, looking for native libraries.
+   * Symlinks any libraries it finds to the main RetroArch "cores" folder,
+   * updating any existing symlinks with the correct path to the native libraries.
+   *
+   * This is necessary because Dynamic Feature Modules are first downloaded
+   * and installed to a temporary location on disk, before being moved
+   * to a more permanent location by the system at a later point.
+   *
+   * This could probably be done in native code instead, if that's preferred.
+   *
+   * @param file The parent directory of the tree to traverse.
+   */
+  private void traverseFilesystem(File file) {
+    if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
+
+    File[] list = file.listFiles();
+    if(list == null) return;
+
+    List<String> availableCores = Arrays.asList(getAvailableCores());
+
+    // Check each file in a directory to see if it's a native library.
+    for(int i = 0; i < list.length; i++) {
+      File child = list[i];
+      String name = child.getName();
+
+      if(name.startsWith("lib") && name.endsWith(".so") && !name.contains("retroarch-activity")) {
+        // Found a native library!
+        String core = name.subSequence(3, name.length() - 3).toString();
+        String filename = child.getAbsolutePath();
+
+        SharedPreferences prefs = UserPreferences.getPreferences(this);
+        if(!prefs.getBoolean("core_deleted_" + core, false)
+                && availableCores.contains(core)) {
+          // Generate the destination filename and delete any existing symlinks / cores
+          String newFilename = getCorePath() + core + "_libretro_android.so";
+          new File(newFilename).delete();
+
+          try {
+            Os.symlink(filename, newFilename);
+          } catch (Exception e) {
+            // Symlink failed to be created. Should never happen.
+          }
+        }
+      } else if(file.isDirectory()) {
+        // Found another directory, so traverse it
+        traverseFilesystem(child);
+      }
+    }
+  }
+}

+ 7 - 4
app/src/main/java/com/xugame/gameconsole/MainActivity.java

@@ -125,8 +125,9 @@ public class MainActivity extends PreferenceActivity implements View.OnClickList
 
         startRetroActivity(
                 retro,
-                null,
-                prefs.getString("libretro_path", getApplicationInfo().dataDir + "/cores/"),
+                romPath,
+//                prefs.getString("libretro_path", getApplicationInfo().dataDir + "/cores/fbalpha2012_neogeo_libretro_android.so"),
+                prefs.getString("libretro_path", corePath),
                 UserPreferences.getDefaultConfigPath(this),
                 Settings.Secure.getString(getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD),
                 getApplicationInfo().dataDir,
@@ -148,7 +149,7 @@ public class MainActivity extends PreferenceActivity implements View.OnClickList
         retro.putExtra("SDCARD", Environment.getExternalStorageDirectory().getAbsolutePath());
         String external = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + PACKAGE_NAME + "/files";
         retro.putExtra("EXTERNAL", external);
-        Log.i("TEST_STAT","corePath="+corePath+"\n"+"configFilePath="+configFilePath+"\nimePath="+imePath
+        Log.i("TEST_STAT","rompath="+contentPath+"\ncorePath="+corePath+"\n"+"configFilePath="+configFilePath+"\nimePath="+imePath
                 +"\nDATADIR="+dataDirPath+"\nAPK="+dataSourcePath+
                 "\nSDCARD="+Environment.getExternalStorageDirectory().getAbsolutePath()+"\nEXTERNAL="
                 +external);
@@ -180,13 +181,15 @@ public class MainActivity extends PreferenceActivity implements View.OnClickList
 
         finalStartup();
     }
+    private String corePath,romPath;
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 //        setContentView(R.layout.activity_main);
 //        this.findView();
         PACKAGE_NAME = getPackageName();
-
+        romPath=getIntent().getStringExtra("romPath");
+        corePath=getIntent().getStringExtra("corePath");
         // Bind audio stream to hardware controls.
         setVolumeControlStream(AudioManager.STREAM_MUSIC);
 

+ 197 - 0
app/src/main/java/com/xugame/gameconsole/TextCopyFileActivity.java

@@ -0,0 +1,197 @@
+package com.xugame.gameconsole;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Environment;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+
+import com.xugame.gameconsole.emulator.EmulatorType;
+import com.xugame.gameconsole.util.DebugUtil;
+import com.xugame.gameconsole.util.PermissionsUtils;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class TextCopyFileActivity extends Activity {
+    private static final String TAG = "TextCopyFileActivity";
+    private Button btn1, btn2;
+    private Context mContext;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.textcopp_layout);
+        this.mContext = this;
+        String[] permissions = new String[]{
+                Manifest.permission.WRITE_EXTERNAL_STORAGE,
+                Manifest.permission.CHANGE_NETWORK_STATE,
+                Manifest.permission.CHANGE_WIFI_STATE,
+                Manifest.permission.RECORD_AUDIO,
+                Manifest.permission.MODIFY_AUDIO_SETTINGS,
+                Manifest.permission.WAKE_LOCK
+        };
+//        PermissionsUtils.showSystemSetting = false;//是否支持显示系统设置权限设置窗口跳转
+        //这里的this不是上下文,是Activity对象!
+        PermissionsUtils.getInstance().chekPermissions(this, permissions, permissionsResult);
+
+        init();
+//        		copyCore("fbalpha2012_neogeo_libretro_android.so","/data/user/0/" + getPackageName() + "/cores/");
+    }
+
+    //创建监听权限的接口对象
+    PermissionsUtils.IPermissionsResult permissionsResult = new PermissionsUtils.IPermissionsResult() {
+        @Override
+        public void passPermissons() {
+
+//            Toast.makeText(MainActivity.this, "权限通过,可以做其他事情!", Toast.LENGTH_SHORT).show();
+        }
+
+        @Override
+        public void forbitPermissons() {
+//            finish();
+            Toast.makeText(mContext, "Failed to obtain permissions!", Toast.LENGTH_SHORT).show();
+        }
+    };
+
+    private void init() {
+        btn1 = findViewById(R.id.btn1);
+        btn2 = findViewById(R.id.btn2);
+        btn1.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                ///cores/fbalpha2012_neogeo_libretro_android.so
+                startGame(Environment.getExternalStorageDirectory().getAbsolutePath() + "/arcade/rom/kof97.zip",
+                        mContext.getFilesDir().getParent() + "/cores/fbalpha2012_neogeo_libretro_android.so",
+                        EmulatorType.NEOGEO);
+            }
+        });
+        btn2.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                startGame(Environment.getExternalStorageDirectory().getAbsolutePath() + "/arcade/rom/gigawing.zip",
+                        mContext.getFilesDir().getParent() + "/cores/fbalpha2012_cps2_libretro_android.so",
+                        EmulatorType.CPS2);
+
+            }
+        });
+    }
+
+    private void startGame(String rompath, String corepath, EmulatorType Type) {
+        File file = new File(corepath);
+        if (file.exists()) {
+            Intent intent = new Intent(this, MainActivity.class);
+            intent.putExtra("romPath", rompath);
+            intent.putExtra("corePath", corepath);
+            startActivity(intent);
+        } else {
+            DebugUtil.i(TAG, "core 不存在");
+            String coreName = "";
+            switch (Type) {
+                case CPS2:
+                    coreName = "fbalpha2012_cps2_libretro_android.so";
+                    break;
+                case NEOGEO:
+                    coreName = "fbalpha2012_neogeo_libretro_android.so";
+                    break;
+            }
+            if (!TextUtils.isEmpty(coreName))
+                startCopy(coreName,
+                        "/data/user/0/" + getPackageName() + "/cores/");
+
+        }
+    }
+
+    private void showDialog(final String fileName, final String toPath) {
+        AlertDialog.Builder alertDialog = new ProgressDialog.Builder(this)
+                .setTitle("Yes/No")
+                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        startCopy(fileName, toPath);
+                        dialog.dismiss();
+                    }
+                })
+                .setNegativeButton("No", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dialog.dismiss();
+                    }
+                })
+                .setMessage("是否复制" + fileName);
+        alertDialog.show();
+    }
+
+    private void startCopy(final String fileName, final String toPath) {
+        File file = new File(this.getFilesDir().getParent() + File.separator + "cores");
+        if (!file.exists())
+            file.mkdirs();
+        Log.i(TAG, "file=" + file.getPath());
+        showProgress();
+        new AsyncTask<String, Integer, String>() {
+            @Override
+            protected String doInBackground(String... strings) {
+                copyFile("/data/user/0/com.xugame.gameconsole/cores/" + fileName, fileName);
+                return null;
+            }
+
+            @Override
+            protected void onPostExecute(String s) {
+                Log.i(TAG, "onPostExecute");
+                if(mProgressDialog!=null)
+                    mProgressDialog.dismiss();
+                Toast.makeText(mContext, "核心安装完成", Toast.LENGTH_SHORT).show();
+                super.onPostExecute(s);
+            }
+        }.execute("");
+    }
+
+    private boolean copyFile(String file_path, String fileName) {
+        Log.i(TAG, "copyFile" + "filepath=" + file_path);
+        try {
+            InputStream in = null;
+            in = this.getResources().getAssets().open(fileName);
+            BufferedOutputStream outStream
+                    = new BufferedOutputStream(new FileOutputStream(file_path, false));
+            byte[] buffer = new byte[1024];
+            int size;
+            while ((size = in.read(buffer)) > 0) {
+                outStream.write(buffer, 0, size);
+            }
+            in.close();
+            outStream.flush();
+            outStream.close();
+
+            return true;
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        return false;
+    }
+
+    ProgressDialog mProgressDialog;
+
+    private void showProgress() {
+        mProgressDialog = ProgressDialog.show(this, "核心安装",
+                "正在安装核心。。。");
+        mProgressDialog.show();
+    }
+
+}

+ 19 - 2
app/src/main/java/com/xugame/gameconsole/emulator/BaseRetroArchEmulator.java

@@ -22,12 +22,14 @@ import android.system.Os;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
 import android.view.InputDevice;
+import android.view.InputQueue;
 import android.view.Surface;
 import android.view.WindowManager;
 
 import com.xugame.BuildConfig;
 import com.xugame.gameconsole.playcore.PlayCoreManager;
 import com.xugame.gameconsole.preferences.UserPreferences;
+import com.xugame.gameconsole.util.DebugUtil;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -37,7 +39,7 @@ import java.util.Locale;
 import java.util.concurrent.CountDownLatch;
 
 public class BaseRetroArchEmulator extends NativeActivity {
-
+    private static final String TAG = "BaseRetroArchEmulatorTAG";
 
     static {
         System.loadLibrary("retroarch-activity");
@@ -57,12 +59,27 @@ public class BaseRetroArchEmulator extends NativeActivity {
     public boolean sustainedPerformanceMode = true;
     public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 
+    @Override
+    public void onInputQueueCreated(InputQueue queue) {
+
+        if(queue!=null){
+            DebugUtil.i(TAG,"onInputQueueCreated"+ queue);
+        }
+        super.onInputQueueCreated(queue);
+    }
+
+    @Override
+    public void onInputQueueDestroyed(InputQueue queue) {
+        DebugUtil.i(TAG,"onInputQueueDestroyed");
+        super.onInputQueueDestroyed(queue);
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         cleanupSymlinks();
         updateSymlinks();
 
-        PlayCoreManager.getInstance().onCreate(this);
+//        PlayCoreManager.getInstance().onCreate(this);
         super.onCreate(savedInstanceState);
     }
 

+ 8 - 0
app/src/main/java/com/xugame/gameconsole/emulator/EmulatorType.java

@@ -0,0 +1,8 @@
+package com.xugame.gameconsole.emulator;
+
+public enum EmulatorType {
+    NEOGEO,
+    CPS1,
+    CPS2,
+    CPS3
+}

+ 2 - 1
app/src/main/java/com/xugame/gameconsole/emulator/RetroActivityCamera.java

@@ -7,11 +7,12 @@ import android.os.Build;
 import android.os.Bundle;
 import android.util.Log;
 
+import com.retroarch.browser.retroactivity.RetroActivityCommon;
 import com.xugame.gameconsole.preferences.UserPreferences;
 
 import java.io.IOException;
 
-public class RetroActivityCamera extends BaseRetroArchEmulator{
+public class RetroActivityCamera extends RetroActivityCommon {
 
     private Camera mCamera = null;
     private long lastTimestamp = 0;

+ 43 - 0
app/src/main/java/com/xugame/gameconsole/emulator/RetroArchEmulatorActivity.java

@@ -1,14 +1,18 @@
 package com.xugame.gameconsole.emulator;
 
 import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.DialogInterface;
 import android.content.Intent;
 import android.hardware.input.InputManager;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.util.Log;
+import android.view.KeyEvent;
 import android.view.View;
 import android.view.WindowManager;
 
@@ -18,6 +22,7 @@ import com.xugame.gameconsole.MainActivity;
 import com.xugame.gameconsole.R;
 import com.xugame.gameconsole.preferences.ConfigFile;
 import com.xugame.gameconsole.preferences.UserPreferences;
+import com.xugame.gameconsole.util.DebugUtil;
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -30,6 +35,7 @@ import java.lang.reflect.Method;
 //        configPath = intent.getStringExtra("config_path");
 //        }
 public class RetroArchEmulatorActivity extends RetroActivityCamera {
+    private static final String TAG = "RetroArchEmulatorActivityTAG";
 
     // If set to true then Retroarch will completely exit when it loses focus
     private boolean quitfocus = false;
@@ -121,4 +127,41 @@ public class RetroArchEmulatorActivity extends RetroActivityCamera {
         // If QUITFOCUS parameter was set then completely exit Retroarch when focus is lost
         if (quitfocus) System.exit(0);
     }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        DebugUtil.i(TAG,""+event.getKeyCode());
+        if(event.getKeyCode()==KeyEvent.KEYCODE_BACK
+        &&event.getAction()==KeyEvent.ACTION_DOWN){
+            showDialog();
+            return true;
+        }
+        return super.dispatchKeyEvent(event);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        DebugUtil.i(TAG,"onKeyDown="+event.getKeyCode());
+        return super.onKeyDown(keyCode, event);
+    }
+
+    private void showDialog() {
+        AlertDialog.Builder alertDialog = new ProgressDialog.Builder(this)
+                .setTitle("Yes/No")
+                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        clickBack();
+                        dialog.dismiss();
+                    }
+                })
+                .setNegativeButton("No", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dialog.dismiss();
+                    }
+                })
+                .setMessage("退出游戏?");
+        alertDialog.show();
+    }
 }

+ 2 - 1
app/src/main/java/com/xugame/gameconsole/playcore/PlayCoreManager.java

@@ -1,5 +1,6 @@
 package com.xugame.gameconsole.playcore;
 
+import com.retroarch.browser.retroactivity.RetroActivityCommon;
 import com.xugame.gameconsole.emulator.BaseRetroArchEmulator;
 
 public class PlayCoreManager {
@@ -16,7 +17,7 @@ public class PlayCoreManager {
         return instance;
     }
 
-    public void onCreate(BaseRetroArchEmulator newActivity) {}
+    public void onCreate(RetroActivityCommon newActivity) {}
     public void onDestroy() {}
 
     public String[] getInstalledModules() {

+ 18 - 0
app/src/main/java/com/xugame/gameconsole/util/DebugUtil.java

@@ -0,0 +1,18 @@
+package com.xugame.gameconsole.util;
+
+import android.util.Log;
+
+
+public class DebugUtil {
+    static private boolean DEBUG = true;
+
+    public static void d(String tag, String msg) {
+        if (DEBUG)
+            Log.i(tag, "-++-++-++- " + msg + " -++-++-++-");
+    }
+
+    public static void i(String tag, String msg) {
+        if (DEBUG)
+            Log.i(tag, "-++-++-++- " + msg + " -++-++-++-");
+    }
+}

+ 127 - 0
app/src/main/java/com/xugame/gameconsole/util/PermissionsUtils.java

@@ -0,0 +1,127 @@
+package com.xugame.gameconsole.util;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PermissionsUtils {
+    private final int mRequestCode = 100;//权限请求码
+    public static boolean showSystemSetting = true;
+    private PermissionsUtils() {
+    }
+    private static PermissionsUtils permissionsUtils;
+    private IPermissionsResult mPermissionsResult;
+    public static PermissionsUtils getInstance() {
+        if (permissionsUtils == null) {
+            permissionsUtils = new PermissionsUtils();
+        }
+        return permissionsUtils;
+    }
+    public void chekPermissions(Activity context, String[] permissions, @NonNull IPermissionsResult permissionsResult) {
+        mPermissionsResult = permissionsResult;
+        if (Build.VERSION.SDK_INT < 23) {
+            //6.0才用动态权限
+            permissionsResult.passPermissons();
+            return;
+        }
+        //创建一个mPermissionList,逐个判断哪些权限未授予,未授予的权限存储到mPerrrmissionList中
+        List<String> mPermissionList = new ArrayList<>();
+        //逐个判断你要的权限是否已经通过
+        for (int i = 0; i < permissions.length; i++) {
+            if (ContextCompat.checkSelfPermission(context, permissions[i]) != PackageManager.PERMISSION_GRANTED) {
+                mPermissionList.add(permissions[i]);//添加还未授予的权限
+            }
+        }
+        //申请权限
+        if (mPermissionList.size() > 0) {
+            //有权限没有通过,需要申请
+            ActivityCompat.requestPermissions(context, permissions, mRequestCode);
+        } else {
+            //说明权限都已经通过,可以做你想做的事情去
+            permissionsResult.passPermissons();
+            return;
+        }
+    }
+    //请求权限后回调的方法
+    //参数: requestCode  是我们自己定义的权限请求码
+    //参数: permissions  是我们请求的权限名称数组
+    //参数: grantResults 是我们在弹出页面后是否允许权限的标识数组,数组的长度对应的是权限名称数组的长度,数组的数据0表示允许权限,-1表示我们点击了禁止权限
+    public void onRequestPermissionsResult(Activity context, int requestCode, @NonNull String[] permissions,
+                                           @NonNull int[] grantResults) {
+        boolean hasPermissionDismiss = false;//有权限没有通过
+        if (mRequestCode == requestCode) {
+            for (int i = 0; i < grantResults.length; i++) {
+                if (grantResults[i] == -1) {
+                    hasPermissionDismiss = true;
+                }
+            }
+            //如果有权限没有被允许
+            if (hasPermissionDismiss) {
+                if (showSystemSetting) {
+                    showSystemPermissionsSettingDialog(context);//跳转到系统设置权限页面,或者直接关闭页面,不让他继续访问
+                } else {
+                    mPermissionsResult.forbitPermissons();
+                }
+            } else {
+                //全部权限通过,可以进行下一步操作。。。
+                mPermissionsResult.passPermissons();
+            }
+        }
+    }
+    /**
+     * 不再提示权限时的展示对话框
+     */
+    AlertDialog mPermissionDialog;
+    private void showSystemPermissionsSettingDialog(final Activity context) {
+        final String mPackName = context.getPackageName();
+        if (mPermissionDialog == null) {
+            mPermissionDialog = new AlertDialog.Builder(context)
+                    .setMessage("已禁用权限,请手动授予")
+                    .setPositiveButton("设置", new DialogInterface.OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialog, int which) {
+                            cancelPermissionDialog();
+                            Uri packageURI = Uri.parse("package:" + mPackName);
+                            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI);
+                            context.startActivity(intent);
+                            context.finish();
+                        }
+                    })
+                    .setNegativeButton("取消", new DialogInterface.OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialog, int which) {
+                            //关闭页面或者做其他操作
+                            cancelPermissionDialog();
+                            //mContext.finish();
+                            mPermissionsResult.forbitPermissons();
+                        }
+                    })
+                    .create();
+        }
+        mPermissionDialog.show();
+    }
+    //关闭对话框
+    private void cancelPermissionDialog() {
+        if (mPermissionDialog != null) {
+            mPermissionDialog.cancel();
+            mPermissionDialog = null;
+        }
+    }
+    public interface IPermissionsResult {
+        void passPermissons();
+        void forbitPermissons();
+    }
+
+}

+ 142 - 0
app/src/main/java/com/xugame/gameconsole/util/ZipUtil.java

@@ -0,0 +1,142 @@
+package com.xugame.gameconsole.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+public class ZipUtil {
+
+	public static String unzipRecordFile(String zipFile, String unzipToPath)
+			throws Exception {
+		
+		String mTempRecRepFile = "";
+		
+		ZipInputStream inZip = new ZipInputStream(new FileInputStream(zipFile));
+		ZipEntry zipEntry;
+		String szName = "";
+
+		while ((zipEntry = inZip.getNextEntry()) != null) {
+
+			szName = zipEntry.getName();
+			if (zipEntry.isDirectory()) {
+				szName = szName.substring(0, szName.length() - 1);
+				File folder = new File(unzipToPath + File.separator + szName);
+				folder.mkdirs();
+			} else {
+				//String filePath;
+				//filePath = unzipToPath + File.separator + szName;
+				mTempRecRepFile = unzipToPath + File.separator + "tempReplay.record";
+				File file = new File(mTempRecRepFile);
+				if(file.exists()) file.delete();
+				//此处需要判断文件夹是否存在,不存在则需要创建
+				file.createNewFile();
+				FileOutputStream out = new FileOutputStream(file);
+				int len;
+				final int BUFFERLEN = 1024*2;
+				byte[] buffer = new byte[BUFFERLEN];
+				while ((len = inZip.read(buffer)) != -1) {
+					out.write(buffer, 0, len);
+					out.flush();
+				}
+				try {
+					inZip.closeEntry();
+				} catch (Exception e) {
+					// TODO: handle exception
+					e.printStackTrace();
+				}
+				out.flush();
+				out.close();
+			}
+		}
+		inZip.close();
+		return mTempRecRepFile;
+	}
+
+	public static void unzipFile(String zipFile, String unzipToPath)
+			throws Exception {
+
+		ZipInputStream inZip = new ZipInputStream(new FileInputStream(zipFile));
+		ZipEntry zipEntry;
+		String szName = "";
+
+		while ((zipEntry = inZip.getNextEntry()) != null) {
+
+			szName = zipEntry.getName();
+			if (zipEntry.isDirectory()) {
+				szName = szName.substring(0, szName.length() - 1);
+				File folder = new File(unzipToPath + File.separator + szName);
+				folder.mkdirs();
+			} else {
+				String filePath;
+				filePath = unzipToPath + File.separator + szName;
+				File file = new File(filePath);
+				//此处需要判断文件夹是否存在,不存在则需要创建
+				file.createNewFile();
+				FileOutputStream out = new FileOutputStream(file);
+				int len;
+				final int BUFFERLEN = 1024*2;
+				byte[] buffer = new byte[BUFFERLEN];
+				while ((len = inZip.read(buffer)) != -1) {
+					out.write(buffer, 0, len);
+					out.flush();
+				}
+				try {
+					inZip.closeEntry();
+				} catch (Exception e) {
+					// TODO: handle exception
+					e.printStackTrace();
+				}
+				out.flush();
+				out.close();
+			}
+		}
+		inZip.close();
+	}
+	
+	public static void zipFolder(String srcFilePath, String zipFilePath)
+			throws Exception {
+		// 创建Zip包
+		ZipOutputStream outZip = new ZipOutputStream(new FileOutputStream(
+				zipFilePath));
+		File file = new File(srcFilePath);
+		zipFiles(file.getParent() + File.separator, file.getName(), outZip);
+		outZip.finish();
+		outZip.close();
+	}
+	private static void zipFiles(String folderPath, String filePath,
+                                 ZipOutputStream zipOut) throws Exception {
+
+		if (zipOut == null) {
+			return;
+		}
+		File file = new File(folderPath + filePath);
+		if (file.isFile()) {
+			ZipEntry zipEntry = new ZipEntry(filePath);
+			FileInputStream inputStream = new FileInputStream(file);
+			zipOut.putNextEntry(zipEntry);
+			int len;
+			final int BUFFERLEN = 1024*2;
+			byte[] buffer = new byte[BUFFERLEN];
+			while ((len = inputStream.read(buffer)) != -1) {
+				zipOut.write(buffer, 0, len);
+			}
+			inputStream.close();
+			zipOut.closeEntry();
+		} else {
+			// 文件夹的方式,获取文件夹下的子文件
+			String fileList[] = file.list();
+			// 添加文件夹本身
+			ZipEntry zipEntry = new ZipEntry(filePath + File.separator);
+			zipOut.putNextEntry(zipEntry);
+			zipOut.closeEntry();
+			// 如果有子文件, 遍历子文件
+			for (int i = 0; i < fileList.length; i++) {
+				zipFiles(folderPath, filePath + File.separator + fileList[i],
+						zipOut);
+			}
+		}
+	}
+}

+ 18 - 0
app/src/main/res/layout/textcopp_layout.xml

@@ -0,0 +1,18 @@
+<?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">
+
+    <Button
+        android:id="@+id/btn1"
+        android:layout_width="wrap_content"
+        android:text="kof97"
+        android:layout_height="wrap_content"></Button>
+
+    <Button
+        android:id="@+id/btn2"
+        android:layout_below="@+id/btn1"
+        android:layout_width="wrap_content"
+        android:text="GIGAWing"
+        android:layout_height="wrap_content"></Button>
+</RelativeLayout>

+ 1 - 1
app/src/main/res/values/strings.xml

@@ -1,3 +1,3 @@
 <resources>
-    <string name="app_name">测试掌机</string>
+    <string name="app_name">约战掌机</string>
 </resources>