From 2ea046cd80138cb212942c78786c4956334d83af Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Tue, 18 Jul 2017 00:59:22 +0800 Subject: [PATCH] Add flashing screen --- app/src/main/AndroidManifest.xml | 31 +++--- .../com/topjohnwu/magisk/FlashActivity.java | 98 +++++++++++++++++++ .../com/topjohnwu/magisk/MagiskFragment.java | 32 +++++- .../com/topjohnwu/magisk/ModulesFragment.java | 8 +- .../topjohnwu/magisk/SettingsActivity.java | 6 +- .../magisk/adapters/ApplicationAdapter.java | 5 +- .../com/topjohnwu/magisk/asyncs/FlashZip.java | 91 ++++++----------- .../topjohnwu/magisk/asyncs/MagiskHide.java | 2 - .../magisk/asyncs/ProcessMagiskZip.java | 56 ----------- .../magisk/asyncs/ProcessRepoZip.java | 7 +- .../topjohnwu/magisk/utils/AdaptiveList.java | 36 +++++++ .../com/topjohnwu/magisk/utils/Utils.java | 6 +- app/src/main/res/layout/activity_flash.xml | 56 +++++++++++ .../main/res/layout/list_item_flashlog.xml | 9 ++ app/src/main/res/values/strings.xml | 1 + 15 files changed, 293 insertions(+), 151 deletions(-) create mode 100644 app/src/main/java/com/topjohnwu/magisk/FlashActivity.java delete mode 100644 app/src/main/java/com/topjohnwu/magisk/asyncs/ProcessMagiskZip.java create mode 100644 app/src/main/java/com/topjohnwu/magisk/utils/AdaptiveList.java create mode 100644 app/src/main/res/layout/activity_flash.xml create mode 100644 app/src/main/res/layout/list_item_flashlog.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7c0114162..7b020b563 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,7 @@ - + @@ -22,8 +21,7 @@ - + android:exported="true" /> + - + android:theme="@style/AppTheme.Transparent" /> + + - - + - - + android:exported="true" + android:permission="android.permission.BIND_JOB_SERVICE" /> - + \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/FlashActivity.java b/app/src/main/java/com/topjohnwu/magisk/FlashActivity.java new file mode 100644 index 000000000..b179f40dd --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/FlashActivity.java @@ -0,0 +1,98 @@ +package com.topjohnwu.magisk; + +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.topjohnwu.magisk.asyncs.FlashZip; +import com.topjohnwu.magisk.components.Activity; +import com.topjohnwu.magisk.utils.AdaptiveList; +import com.topjohnwu.magisk.utils.Shell; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +public class FlashActivity extends Activity { + + @BindView(R.id.toolbar) Toolbar toolbar; + @BindView(R.id.flash_logs) RecyclerView flashLogs; + @BindView(R.id.button_panel) LinearLayout buttonPanel; + + private AdaptiveList rootShellOutput; + + @OnClick(R.id.no_thanks) + public void dismiss() { + finish(); + } + + @OnClick(R.id.reboot) + public void reboot() { + Shell.getRootShell(this).su_raw("reboot"); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_flash); + ButterKnife.bind(this); + rootShellOutput = new AdaptiveList<>(flashLogs); + setSupportActionBar(toolbar); + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setTitle(R.string.flashing); + } + setFloating(); + + flashLogs.setAdapter(new FlashLogAdapter()); + + // We must receive a Uri of the target zip + Uri uri = getIntent().getData(); + + new FlashZip(this, uri, rootShellOutput) + .setCallBack(() -> buttonPanel.setVisibility(View.VISIBLE)) + .exec(); + } + + @Override + public void onBackPressed() { + // Prevent user accidentally press back button + } + + private class FlashLogAdapter extends RecyclerView.Adapter { + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.list_item_flashlog, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + holder.text.setText(rootShellOutput.get(position)); + } + + @Override + public int getItemCount() { + return rootShellOutput.size(); + } + } + + public class ViewHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.textView) TextView text; + + public ViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/MagiskFragment.java b/app/src/main/java/com/topjohnwu/magisk/MagiskFragment.java index 77568ffb4..cbfb557be 100644 --- a/app/src/main/java/com/topjohnwu/magisk/MagiskFragment.java +++ b/app/src/main/java/com/topjohnwu/magisk/MagiskFragment.java @@ -26,7 +26,7 @@ import android.widget.Spinner; import android.widget.TextView; import com.topjohnwu.magisk.asyncs.CheckUpdates; -import com.topjohnwu.magisk.asyncs.ProcessMagiskZip; +import com.topjohnwu.magisk.asyncs.ParallelTask; import com.topjohnwu.magisk.components.AlertDialogBuilder; import com.topjohnwu.magisk.components.Fragment; import com.topjohnwu.magisk.components.SnackbarMaker; @@ -144,7 +144,13 @@ public class MagiskFragment extends Fragment @Override public void onDownloadDone(Uri uri, Context context) { - new ProcessMagiskZip(getActivity(), uri, boot, enc, verity).exec(); + if (Shell.rootAccess()) { + new SetInstallFlags(boot, enc, verity) + .setCallBack(() -> startActivity(new Intent(context, FlashActivity.class).setData(uri))) + .exec(context); + } else { + Utils.showUriSnack(getActivity(), uri); + } } }, magiskManager.magiskLink, @@ -160,6 +166,28 @@ public class MagiskFragment extends Fragment .show(); } + private static class SetInstallFlags extends ParallelTask { + + private String boot; + private boolean enc, verity; + + SetInstallFlags(String boot, boolean enc, boolean verity) { + this.boot = boot; + this.enc = enc; + this.verity = verity; + } + + @Override + protected Void doInBackground(Context... contexts) { + Shell.getRootShell(contexts[0]).su_raw("rm -f /dev/.magisk", + (boot != null) ? "echo \"BOOTIMAGE=" + boot + "\" >> /dev/.magisk" : "", + "echo \"KEEPFORCEENCRYPT=" + String.valueOf(enc) + "\" >> /dev/.magisk", + "echo \"KEEPVERITY=" + String.valueOf(verity) + "\" >> /dev/.magisk" + ); + return null; + } + } + @OnClick(R.id.uninstall_button) public void uninstall() { new AlertDialogBuilder(getActivity()) diff --git a/app/src/main/java/com/topjohnwu/magisk/ModulesFragment.java b/app/src/main/java/com/topjohnwu/magisk/ModulesFragment.java index 34907db8d..758a05e5a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ModulesFragment.java +++ b/app/src/main/java/com/topjohnwu/magisk/ModulesFragment.java @@ -2,7 +2,6 @@ package com.topjohnwu.magisk; import android.app.Activity; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; @@ -14,13 +13,11 @@ import android.view.ViewGroup; import android.widget.TextView; import com.topjohnwu.magisk.adapters.ModulesAdapter; -import com.topjohnwu.magisk.asyncs.FlashZip; import com.topjohnwu.magisk.asyncs.LoadModules; import com.topjohnwu.magisk.components.Fragment; import com.topjohnwu.magisk.module.Module; import com.topjohnwu.magisk.utils.CallbackEvent; import com.topjohnwu.magisk.utils.Logger; -import com.topjohnwu.magisk.utils.Shell; import java.util.ArrayList; import java.util.List; @@ -87,8 +84,9 @@ public class ModulesFragment extends Fragment implements CallbackEvent.Listener< public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == FETCH_ZIP_CODE && resultCode == Activity.RESULT_OK && data != null) { // Get the URI of the selected file - final Uri uri = data.getData(); - new FlashZip(getActivity(), uri).exec(); + Intent intent = new Intent(getActivity(), FlashActivity.class); + intent.setData(data.getData()).putExtra("ACTION", "flash"); + startActivity(intent); } } diff --git a/app/src/main/java/com/topjohnwu/magisk/SettingsActivity.java b/app/src/main/java/com/topjohnwu/magisk/SettingsActivity.java index 8f1726fb9..8e2a42ee7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/SettingsActivity.java +++ b/app/src/main/java/com/topjohnwu/magisk/SettingsActivity.java @@ -162,14 +162,14 @@ public class SettingsActivity extends Activity { new AlertDialogBuilder(getActivity()) .setTitle(R.string.no_magisksu_title) .setMessage(R.string.no_magisksu_msg) - .setPositiveButton(R.string.understand, (dialog, which) -> new MagiskHide().enable()) + .setPositiveButton(R.string.understand, (dialog, which) -> new MagiskHide(getActivity()).enable()) .setCancelable(false) .show(); } else { - new MagiskHide().enable(); + new MagiskHide(getActivity()).enable(); } } else { - new MagiskHide().disable(); + new MagiskHide(getActivity()).disable(); } break; case "hosts": diff --git a/app/src/main/java/com/topjohnwu/magisk/adapters/ApplicationAdapter.java b/app/src/main/java/com/topjohnwu/magisk/adapters/ApplicationAdapter.java index 90dcfe778..228e7f6e7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/adapters/ApplicationAdapter.java +++ b/app/src/main/java/com/topjohnwu/magisk/adapters/ApplicationAdapter.java @@ -1,5 +1,6 @@ package com.topjohnwu.magisk.adapters; +import android.app.Activity; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.support.design.widget.Snackbar; @@ -86,10 +87,10 @@ public class ApplicationAdapter extends RecyclerView.Adapter { if (isChecked) { - new MagiskHide().add(info.packageName); + new MagiskHide((Activity) holder.itemView.getContext()).add(info.packageName); mHideList.add(info.packageName); } else { - new MagiskHide().rm(info.packageName); + new MagiskHide((Activity) holder.itemView.getContext()).rm(info.packageName); mHideList.remove(info.packageName); } }); diff --git a/app/src/main/java/com/topjohnwu/magisk/asyncs/FlashZip.java b/app/src/main/java/com/topjohnwu/magisk/asyncs/FlashZip.java index c7fa300ff..df9f48aa2 100644 --- a/app/src/main/java/com/topjohnwu/magisk/asyncs/FlashZip.java +++ b/app/src/main/java/com/topjohnwu/magisk/asyncs/FlashZip.java @@ -1,14 +1,12 @@ package com.topjohnwu.magisk.asyncs; import android.app.Activity; -import android.app.ProgressDialog; import android.net.Uri; -import android.widget.Toast; +import android.text.TextUtils; import com.topjohnwu.magisk.MagiskManager; import com.topjohnwu.magisk.R; -import com.topjohnwu.magisk.components.AlertDialogBuilder; -import com.topjohnwu.magisk.utils.Logger; +import com.topjohnwu.magisk.utils.AdaptiveList; import com.topjohnwu.magisk.utils.Utils; import com.topjohnwu.magisk.utils.ZipUtils; @@ -26,11 +24,12 @@ public class FlashZip extends ParallelTask { private File mCachedFile, mScriptFile, mCheckFile; private String mFilename; - private ProgressDialog progress; + private AdaptiveList mList; - public FlashZip(Activity context, Uri uri) { + public FlashZip(Activity context, Uri uri, AdaptiveList list) { super(context); mUri = uri; + mList = list; mCachedFile = new File(magiskManager.getCacheDir(), "install.zip"); mScriptFile = new File(magiskManager.getCacheDir(), "/META-INF/com/google/android/update-binary"); @@ -40,97 +39,77 @@ public class FlashZip extends ParallelTask { mFilename = Utils.getNameFromUri(magiskManager, mUri); } - private void copyToCache() throws Throwable { - publishProgress(magiskManager.getString(R.string.copying_msg)); + private void copyToCache() throws Exception { + mList.add(magiskManager.getString(R.string.copying_msg)); - if (mCachedFile.exists() && !mCachedFile.delete()) { - Logger.error("FlashZip: Error while deleting already existing file"); - throw new IOException(); - } + mCachedFile.delete(); try ( - InputStream in = magiskManager.getContentResolver().openInputStream(mUri); - OutputStream outputStream = new FileOutputStream(mCachedFile) + InputStream in = magiskManager.getContentResolver().openInputStream(mUri); + OutputStream outputStream = new FileOutputStream(mCachedFile) ) { byte buffer[] = new byte[1024]; int length; if (in == null) throw new FileNotFoundException(); while ((length = in.read(buffer)) > 0) outputStream.write(buffer, 0, length); - - Logger.dev("FlashZip: File created successfully - " + mCachedFile.getPath()); } catch (FileNotFoundException e) { - Logger.error("FlashZip: Invalid Uri"); + mList.add("! Invalid Uri"); throw e; } catch (IOException e) { - Logger.error("FlashZip: Error in creating file"); + mList.add("! Cannot copy to cache"); throw e; } } private boolean unzipAndCheck() throws Exception { ZipUtils.unzip(mCachedFile, mCachedFile.getParentFile(), "META-INF/com/google/android"); - List ret; - ret = Utils.readFile(magiskManager.rootShell, mCheckFile.getPath()); + List ret = Utils.readFile(magiskManager.rootShell, mCheckFile.getPath()); return Utils.isValidShellResponse(ret) && ret.get(0).contains("#MAGISK"); } - private int cleanup(int ret) { - magiskManager.rootShell.su_raw( - "rm -rf " + mCachedFile.getParent() + "/*", - "rm -rf " + MagiskManager.TMP_FOLDER_PATH - ); - return ret; - } - @Override protected void onPreExecute() { - progress = new ProgressDialog(activity); - progress.setTitle(R.string.zip_install_progress_title); - progress.show(); + // UI updates must run in the UI thread + mList.setCallback(this::publishProgress); } @Override protected void onProgressUpdate(String... values) { - progress.setMessage(values[0]); + mList.updateView(); } @Override protected Integer doInBackground(Void... voids) { - Logger.dev("FlashZip Running... " + mFilename); - List ret; try { copyToCache(); - if (!unzipAndCheck()) return cleanup(0); - publishProgress(magiskManager.getString(R.string.zip_install_progress_msg, mFilename)); - ret = magiskManager.rootShell.su( - "BOOTMODE=true sh " + mScriptFile + " dummy 1 " + mCachedFile, - "if [ $? -eq 0 ]; then echo true; else echo false; fi" + if (!unzipAndCheck()) return 0; + mList.add(magiskManager.getString(R.string.zip_install_progress_msg, mFilename)); + magiskManager.rootShell.su(mList, + "BOOTMODE=true sh " + mScriptFile + " dummy 1 " + mCachedFile + + " && echo 'Success!' || echo 'Failed!'" ); - if (!Utils.isValidShellResponse(ret)) return -1; - Logger.dev("FlashZip: Console log:"); - for (String line : ret) { - Logger.dev(line); - } - if (Boolean.parseBoolean(ret.get(ret.size() - 1))) - return cleanup(1); - - } catch (Throwable e) { + if (TextUtils.equals(mList.get(mList.size() - 1), "Success!")) + return 1; + } catch (Exception e) { e.printStackTrace(); } - return cleanup(-1); + return -1; } // -1 = error, manual install; 0 = invalid zip; 1 = success @Override protected void onPostExecute(Integer result) { - progress.dismiss(); + magiskManager.rootShell.su_raw( + "rm -rf " + mCachedFile.getParent() + "/*", + "rm -rf " + MagiskManager.TMP_FOLDER_PATH + ); switch (result) { case -1: - Toast.makeText(magiskManager, magiskManager.getString(R.string.install_error), Toast.LENGTH_LONG).show(); + mList.add(magiskManager.getString(R.string.install_error)); Utils.showUriSnack(activity, mUri); break; case 0: - Toast.makeText(magiskManager, magiskManager.getString(R.string.invalid_zip), Toast.LENGTH_LONG).show(); + mList.add(magiskManager.getString(R.string.invalid_zip)); break; case 1: onSuccess(); @@ -140,14 +119,6 @@ public class FlashZip extends ParallelTask { } protected void onSuccess() { - magiskManager.updateCheckDone.trigger(); new LoadModules(activity).exec(); - - new AlertDialogBuilder(activity) - .setTitle(R.string.reboot_title) - .setMessage(R.string.reboot_msg) - .setPositiveButton(R.string.reboot, (dialogInterface, i) -> magiskManager.rootShell.su_raw("reboot")) - .setNegativeButton(R.string.no_thanks, null) - .show(); } } diff --git a/app/src/main/java/com/topjohnwu/magisk/asyncs/MagiskHide.java b/app/src/main/java/com/topjohnwu/magisk/asyncs/MagiskHide.java index fea8d36eb..b0fdbe712 100644 --- a/app/src/main/java/com/topjohnwu/magisk/asyncs/MagiskHide.java +++ b/app/src/main/java/com/topjohnwu/magisk/asyncs/MagiskHide.java @@ -8,8 +8,6 @@ public class MagiskHide extends ParallelTask { private boolean isList = false; - public MagiskHide() {} - public MagiskHide(Activity context) { super(context); } diff --git a/app/src/main/java/com/topjohnwu/magisk/asyncs/ProcessMagiskZip.java b/app/src/main/java/com/topjohnwu/magisk/asyncs/ProcessMagiskZip.java deleted file mode 100644 index 39820e031..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/asyncs/ProcessMagiskZip.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.topjohnwu.magisk.asyncs; - -import android.app.Activity; -import android.app.ProgressDialog; -import android.net.Uri; - -import com.topjohnwu.magisk.R; -import com.topjohnwu.magisk.utils.Shell; -import com.topjohnwu.magisk.utils.Utils; - -public class ProcessMagiskZip extends ParallelTask { - - private Uri mUri; - private ProgressDialog progressDialog; - private String mBoot; - private boolean mEnc, mVerity; - - public ProcessMagiskZip(Activity context, Uri uri, String boot, boolean enc, boolean verity) { - super(context); - mUri = uri; - mBoot = boot; - mEnc = enc; - mVerity = verity; - } - - @Override - protected void onPreExecute() { - progressDialog = ProgressDialog.show(activity, - activity.getString(R.string.zip_process_title), - activity.getString(R.string.zip_unzip_msg)); - } - - @Override - protected Boolean doInBackground(Void... params) { - if (Shell.rootAccess()) { - magiskManager.rootShell.su_raw("rm -f /dev/.magisk", - (mBoot != null) ? "echo \"BOOTIMAGE=" + mBoot + "\" >> /dev/.magisk" : "", - "echo \"KEEPFORCEENCRYPT=" + String.valueOf(mEnc) + "\" >> /dev/.magisk", - "echo \"KEEPVERITY=" + String.valueOf(mVerity) + "\" >> /dev/.magisk" - ); - return true; - } - return false; - } - - @Override - protected void onPostExecute(Boolean result) { - progressDialog.dismiss(); - if (result) { - new FlashZip(activity, mUri).exec(); - } else { - Utils.showUriSnack(activity, mUri); - } - super.onPostExecute(result); - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/asyncs/ProcessRepoZip.java b/app/src/main/java/com/topjohnwu/magisk/asyncs/ProcessRepoZip.java index d21cc9464..e60124220 100644 --- a/app/src/main/java/com/topjohnwu/magisk/asyncs/ProcessRepoZip.java +++ b/app/src/main/java/com/topjohnwu/magisk/asyncs/ProcessRepoZip.java @@ -2,9 +2,11 @@ package com.topjohnwu.magisk.asyncs; import android.app.Activity; import android.app.ProgressDialog; +import android.content.Intent; import android.net.Uri; import android.widget.Toast; +import com.topjohnwu.magisk.FlashActivity; import com.topjohnwu.magisk.R; import com.topjohnwu.magisk.utils.Logger; import com.topjohnwu.magisk.utils.Shell; @@ -85,13 +87,12 @@ public class ProcessRepoZip extends ParallelTask { progressDialog.dismiss(); if (result) { if (Shell.rootAccess() && mInstall) { - new FlashZip(activity, mUri).exec(); + magiskManager.startActivity(new Intent(magiskManager, FlashActivity.class).setData(mUri)); } else { Utils.showUriSnack(activity, mUri); } - } else { - Toast.makeText(activity, R.string.process_error, Toast.LENGTH_LONG).show(); + magiskManager.toast(R.string.process_error, Toast.LENGTH_LONG); } } } diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/AdaptiveList.java b/app/src/main/java/com/topjohnwu/magisk/utils/AdaptiveList.java new file mode 100644 index 000000000..aa6d59583 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/AdaptiveList.java @@ -0,0 +1,36 @@ +package com.topjohnwu.magisk.utils; + +import android.support.v7.widget.RecyclerView; + +import java.util.ArrayList; + +public class AdaptiveList extends ArrayList { + + private Runnable callback; + private RecyclerView mView; + + public AdaptiveList(RecyclerView v) { + mView = v; + } + + public void updateView() { + mView.getAdapter().notifyDataSetChanged(); + mView.scrollToPosition(mView.getAdapter().getItemCount() - 1); + } + + public void setCallback(Runnable cb) { + callback = cb; + } + + public boolean add(E e) { + boolean ret = super.add(e); + if (ret) { + if (callback == null) { + updateView(); + } else { + callback.run(); + } + } + return ret; + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java b/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java index 44338b488..4ff21872e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java +++ b/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java @@ -44,19 +44,19 @@ public class Utils { private static final int APK_UPDATE_NOTIFICATION_ID = 2; public static boolean itemExist(Shell shell, String path) { - String command = "if [ -e " + path + " ]; then echo true; else echo false; fi"; + String command = "[ -e " + path + " ] && echo true || echo false"; List ret = shell.su(command); return isValidShellResponse(ret) && Boolean.parseBoolean(ret.get(0)); } public static void createFile(Shell shell, String path) { String folder = path.substring(0, path.lastIndexOf('/')); - String command = "mkdir -p " + folder + " 2>/dev/null; touch " + path + " 2>/dev/null; if [ -f \"" + path + "\" ]; then echo true; else echo false; fi"; + String command = "mkdir -p " + folder + " 2>/dev/null; touch " + path + " 2>/dev/null;"; shell.su_raw(command); } public static void removeItem(Shell shell, String path) { - String command = "rm -rf " + path + " 2>/dev/null; if [ -e " + path + " ]; then echo false; else echo true; fi"; + String command = "rm -rf " + path + " 2>/dev/null"; shell.su_raw(command); } diff --git a/app/src/main/res/layout/activity_flash.xml b/app/src/main/res/layout/activity_flash.xml new file mode 100644 index 000000000..5bcd9fa0a --- /dev/null +++ b/app/src/main/res/layout/activity_flash.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + +