package com.stealth.morphing; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.util.Collections; import java.util.List; import android.annotation.TargetApi; import android.app.Activity; import android.app.Dialog; import android.app.ProgressDialog; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.net.Uri; import android.nfc.NfcAdapter; import android.nfc.NfcEvent; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.support.v4.app.Fragment; import android.text.InputFilter; import android.text.LoginFilter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import com.alexkolpa.appmorphing.AppMorph; import com.ipaulpro.afilechooser.utils.FileUtils; import com.stealth.android.HomeActivity; import com.stealth.android.R; import com.stealth.dialog.DialogConstructor; import com.stealth.dialog.DialogOptions; import com.stealth.dialog.IDialogAdapter; import com.stealth.font.FontManager; import com.stealth.utils.Utils; /** * A simple {@link android.support.v4.app.Fragment} subclass. Use the {@link MorphingFragment#newInstance} factory * method to create an instance of this fragment. */ public class MorphingFragment extends Fragment implements View.OnClickListener, AppMorph.MorphProgressListener { private static final int REQUEST_CHOOSER = 65456; private static final int ICON_SIZE_DP = 48; private List<ApplicationInfo> mPackages; private ImageView mIcon; private EditText mName; private AppMorph mAppMorph; private ProgressDialog mMorphProgressDialog; private NfcAdapter mNfcAdapter; private File mCurrentApp; private String mCurrentLabel; private Uri mCurrentIconPath; private View.OnClickListener mAppPicked = new View.OnClickListener() { @Override public void onClick(View view) { ApplicationInfo ai = (ApplicationInfo) view.getTag(); int iconSize = Utils.px(ICON_SIZE_DP); Bitmap bitmap = convertToBitmap(mPackMan.getApplicationIcon(ai), iconSize, iconSize); File cache = Utils.getRandomCacheFile(".png"); try { bitmap.compress(Bitmap.CompressFormat.PNG, 100, new FileOutputStream(cache)); } catch (FileNotFoundException e) { Utils.toast(R.string.icon_retrieve_failed); } mCurrentIconPath = Uri.fromFile(cache); mCurrentLabel = mPackMan.getApplicationLabel(ai).toString(); mIcon.setImageBitmap(bitmap); mName.setText(mCurrentLabel); Utils.fadein(mIcon, 75); Utils.fadein(mName, 100); mAppDialog.dismiss(); } }; private IDialogAdapter<ApplicationInfo> mAppAdapter = new IDialogAdapter<ApplicationInfo>() { @Override public List<ApplicationInfo> getList() { return mPackages; } @Override public int getItemLayout() { return R.layout.dialog_button; } @Override public void setView(int index, View v) { ApplicationInfo ai = getList().get(index); ((ImageView) v.findViewById(R.id.dialog_button_icon)).setImageDrawable(mPackMan.getApplicationIcon(ai)); ((TextView) v.findViewById(R.id.dialog_button_title)).setText(mPackMan.getApplicationLabel(ai)); v.setTag(ai); // remember the application info in the view v.setOnClickListener(mAppPicked); } }; private PackageManager mPackMan; private Dialog mAppDialog; public MorphingFragment() { // Required empty public constructor } /** * Use this factory method to create a new instance of this fragment using the provided parameters. * * @return A new instance of fragment MorphingFragment. */ public static MorphingFragment newInstance() { MorphingFragment fragment = new MorphingFragment(); return fragment; } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPackMan = getActivity().getPackageManager(); mAppMorph = new AppMorph(getActivity()); mAppMorph.setMorphProgressListener(this); mCurrentApp = new File(getActivity().getPackageResourcePath()); if (getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)) { mNfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); mNfcAdapter.setBeamPushUrisCallback(new FileUriCallback(), getActivity()); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_morphing, container, false); if (root == null) { return null; } View pickApp = root.findViewById(R.id.morph_pick_app); pickApp.setOnClickListener(this); View pickIcon = root.findViewById(R.id.morph_pick_icon); pickIcon.setOnClickListener(this); View share = root.findViewById(R.id.morph_share); share.setOnClickListener(this); View reset = root.findViewById(R.id.morph_reset); reset.setOnClickListener(this); View morph = root.findViewById(R.id.morph_execute); morph.setOnClickListener(this); //TODO: Figure out way to have real max length. In case we pad with spaces. InputFilter lengthFilter = new InputFilter.LengthFilter(getString(R.string.morphable_app_name).length()); InputFilter asciiFilter = new LoginFilter.PasswordFilterGMail(); // Encodes ISO-8859-1 (Extended Ascii) mName = (EditText) root.findViewById(R.id.morph_edit_name); mName.setFilters(new InputFilter[]{lengthFilter, asciiFilter}); mIcon = (ImageView) root.findViewById(R.id.morph_edit_icon); FontManager.handleFontTags(root); return root; } private Bitmap convertToBitmap(Drawable drawable, int widthPixels, int heightPixels) { Bitmap mutableBitmap = Bitmap.createBitmap(widthPixels, heightPixels, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mutableBitmap); drawable.setBounds(0, 0, widthPixels, heightPixels); drawable.draw(canvas); return mutableBitmap; } /** * Get the list of applications, sorted on their label. * * @param packageManager the package manager to get the info from * @return the sorted application list */ public List<ApplicationInfo> getInstalledApplication(PackageManager packageManager) { List<ApplicationInfo> apps = packageManager.getInstalledApplications(0); Collections.sort(apps, new ApplicationInfo.DisplayNameComparator(packageManager)); return apps; } /** * Selects an action based on the view that was clicked * * @param view */ @Override public void onClick(View view) { switch (view.getId()) { case R.id.morph_pick_app: showApplicationPicker(); break; case R.id.morph_pick_icon: pickIcon(); break; case R.id.morph_share: shareApp(); break; case R.id.morph_execute: morphApp(); break; } } /** * Shows the application picker in order to obtain the icon and name of another application */ private void showApplicationPicker() { // do some checks, as without these we can't continue if (getActivity() == null) { return; } if (mPackMan == null) { return; } // get the installed applications if (mPackages == null) { // only load it if we haven't yet mPackages = getInstalledApplication(mPackMan); } // setup the texts and dialog adapter DialogOptions<ApplicationInfo> options = new DialogOptions<ApplicationInfo>() .setTitle(R.string.morph_apppicker_title) .setDescription(R.string.morph_apppicker_description) .setPositiveButtonEnabled(false) .setNegative(R.string.cancel) .setDialogAdapter(mAppAdapter); // Build the dialog mAppDialog = DialogConstructor.show(getActivity(), options, null); } /** * Lets the user pick a new icon from their own folder */ private void pickIcon() { Intent getContentIntent = FileUtils.createGetContentIntent(); getContentIntent.setType("image/*"); Intent intent = Intent.createChooser(getContentIntent, Utils.str(R.string.morph_select_image)); ((HomeActivity) getActivity()).setRequestedActivity(true); startActivityForResult(intent, REQUEST_CHOOSER); } /** * Share currently selected application through an intent */ private void shareApp() { if (mCurrentApp != null) { Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEND); intent.setType("application/zip"); intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(mCurrentApp)); startActivity(intent); } } /** * Sets the currently selected app to share to the */ private void morphApp() { mMorphProgressDialog = ProgressDialog.show(getActivity(), Utils.str(R.string.morphing_dialog_title), Utils.str(R.string.morphing_started), false, false); mCurrentLabel = mName.getText().toString(); new MorphTask(mCurrentLabel, mCurrentIconPath).execute(); } /** * Listens for the return of the get content intent. Adds the items if successful * * @param requestCode * @param resultCode * @param data */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CHOOSER: if (resultCode == Activity.RESULT_OK) { mCurrentIconPath = data.getData(); if (getActivity() == null) { return; } if (getActivity().getApplicationContext() == null) { return; } setImageOnView(mCurrentIconPath, mIcon); } break; } } private void setImageOnView(Uri imageURI, ImageView view) { Bitmap bitmap = loadCroppedBitmapFromUri(imageURI, 256); if (bitmap == null) { //TODO notify user of failure! return; } bitmap = Utils.correctOrientation(bitmap, mCurrentIconPath); view.setImageBitmap(bitmap); Utils.fadein(view, 75); } /** * Loads a Bitmap from the given URI and scales and crops it to the given size * * @param uri The URI to load the Bitmap from * @param size The size the final Bitmap should be * @return */ private Bitmap loadCroppedBitmapFromUri(Uri uri, int size) { File bitmapFile = FileUtils.getFile(getActivity(), uri); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(bitmapFile.getPath(), options); // TODO: Handle null pointers. options.inSampleSize = calculateSampleSize(options, size, size); options.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeFile(bitmapFile.getPath(), options); if (bitmap == null) { Utils.d("Bitmap loading failed!"); return null; } if (bitmap.getHeight() > size || bitmap.getWidth() > size) { bitmap = Utils.crop(bitmap, size, size); } return bitmap; } /** * Helper function to determine the minimum required sample size for a bitmap based on the passed options * * @param options The options from which width and height of the original image are extracted * @param reqHeight height in pixel the resulting image should be * @param reqWidth width in pixels the resulting image should be * @return the sample size required to get an image at least the size of reqHeight and reqWidth */ private static int calculateSampleSize(BitmapFactory.Options options, int reqHeight, int reqWidth) { float cropRatio = (float) reqHeight / (float) reqWidth; float baseRatio = (float) options.outHeight / (float) options.outWidth; //Default to 1 to prevent sampling errors float scale = 1f; if (baseRatio > cropRatio) { scale = (float) reqWidth / (float) options.outWidth; } else { scale = (float) reqHeight / (float) options.outHeight; } return (int) Math.ceil(scale); } @Override public void onAttach(Activity activity) { super.onAttach(activity); } @Override public void onDetach() { super.onDetach(); } @Override public void onProgress(AppMorph.ProgressStep progress) { if (mMorphProgressDialog != null) { String message = null; switch (progress) { case Extracting: message = Utils.str(R.string.morphing_extracting); break; case SettingLabel: message = Utils.str(R.string.morphing_labeling); break; case SettingIcons: message = Utils.str(R.string.morphing_icons); break; case Repackaging: message = Utils.str(R.string.morphing_repackaging); break; case Signing: message = Utils.str(R.string.morphing_signing); break; } if (message != null) { final String finalMessage = message; Utils.runOnMain(new Runnable() { @Override public void run() { if (mMorphProgressDialog != null) { mMorphProgressDialog.setMessage(finalMessage); } } }); } } } @Override public void onMorphFailed(AppMorph.ProgressStep atPoint, Exception failure) { mMorphProgressDialog.dismiss(); mMorphProgressDialog = null; Utils.toast(R.string.morph_failed); Utils.d("Failed: " + atPoint.toString() + " because: " + failure.getLocalizedMessage()); failure.printStackTrace(); } @Override public void onFinished(final File newApk) { Utils.runOnMain(new Runnable() { @Override public void run() { if (mMorphProgressDialog != null) { mMorphProgressDialog.dismiss(); mMorphProgressDialog = null; } if (newApk != null) { mCurrentApp = newApk; View currentView = getView(); if (currentView != null) { TextView label = (TextView) currentView.findViewById(R.id.morph_current_name); label.setText(mCurrentLabel); Utils.fadein(label, 100); ImageView icon = (ImageView) currentView.findViewById(R.id.morph_current_icon); setImageOnView(mCurrentIconPath, icon); } } } }); } private class MorphTask extends AsyncTask<Void, Void, Void> { private String mLabel; private Uri mIcon; public MorphTask(String label, Uri icon) { mLabel = label; mIcon = icon; } @Override protected Void doInBackground(Void... voids) { mAppMorph.morphApp(mLabel, mIcon); return null; } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private class FileUriCallback implements NfcAdapter.CreateBeamUrisCallback { @Override public Uri[] createBeamUris(NfcEvent nfcEvent) { return new Uri[] { Uri.fromFile(mCurrentApp) }; } } }