diff --git a/app/build.gradle b/app/build.gradle index 121cec471..f0fe1579d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,6 +14,7 @@ android { enabled true } } + buildTypes { release { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' @@ -24,13 +25,17 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } } +repositories { + jcenter() + maven { url "https://jitpack.io" } +} dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') compile 'com.android.support:recyclerview-v7:24.2.0' compile 'com.android.support:cardview-v7:24.2.0' compile 'com.android.support:design:24.2.0' - compile 'com.jakewharton:butterknife:8.4.0' + compile 'com.github.michalis-vitos:aFileChooser:master' annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8c4b20faf..941bc7281 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,16 +1,11 @@ - - - - - - - + + + + - + - + + + + + + + + + + + + + + + + + android:theme="@style/AppTheme.Transparent"/> + android:grantUriPermissions="true" + android:exported="false"> - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/AboutActivity.java b/app/src/main/java/com/topjohnwu/magisk/AboutActivity.java index 59542e177..c384fe855 100644 --- a/app/src/main/java/com/topjohnwu/magisk/AboutActivity.java +++ b/app/src/main/java/com/topjohnwu/magisk/AboutActivity.java @@ -24,7 +24,7 @@ import java.io.InputStream; import butterknife.BindView; import butterknife.ButterKnife; -public class AboutActivity extends AppCompatActivity { +public class AboutActivity extends AppCompatActivity { private static final String SOURCE_CODE_URL = "https://github.com/topjohnwu/MagiskManager"; private static final String XDA_THREAD = "http://forum.xda-developers.com/android/software/mod-magisk-v1-universal-systemless-t3432382"; diff --git a/app/src/main/java/com/topjohnwu/magisk/BaseModuleFragment.java b/app/src/main/java/com/topjohnwu/magisk/BaseModuleFragment.java index 9a01bfa7f..e5fe56942 100644 --- a/app/src/main/java/com/topjohnwu/magisk/BaseModuleFragment.java +++ b/app/src/main/java/com/topjohnwu/magisk/BaseModuleFragment.java @@ -1,40 +1,90 @@ package com.topjohnwu.magisk; +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.graphics.Color; import android.os.Bundle; +import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; +import android.util.DisplayMetrics; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowManager; import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.TextView; import com.topjohnwu.magisk.module.Module; +import com.topjohnwu.magisk.module.RepoHelper; +import com.topjohnwu.magisk.utils.Shell; import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.magisk.utils.WebWindow; +import java.io.File; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; public abstract class BaseModuleFragment extends Fragment { + @BindView(R.id.swipeRefreshLayout) + SwipeRefreshLayout mSwipeRefreshLayout; + @BindView(R.id.recyclerView) + RecyclerView recyclerView; + @BindView(R.id.empty_rv) + TextView emptyTv; - @BindView(R.id.recyclerView) RecyclerView recyclerView; - @BindView(R.id.empty_rv) TextView emptyTv; + private RepoHelper.TaskDelegate mDelegate; + private SharedPreferences prefs; + + public BaseModuleFragment SetDelegate(RepoHelper.TaskDelegate delegate) { + mDelegate = delegate; + return null; + } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.single_module_fragment, container, false); - ButterKnife.bind(this, view); + View viewMain = inflater.inflate(R.layout.single_module_fragment, container, false); + + ButterKnife.bind(this, viewMain); + + + prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + mSwipeRefreshLayout.setOnRefreshListener(() -> { + + mDelegate.taskCompletionResult("OK"); + prefs.edit().putBoolean("ignoreUpdateAlerts", false).apply(); + + + }); + prefs.registerOnSharedPreferenceChangeListener((sharedPreferences, s) -> { + if (s.contains("updated")) { + viewMain.invalidate(); + viewMain.requestLayout(); + + } + }); if (listModules().size() == 0) { emptyTv.setVisibility(View.VISIBLE); recyclerView.setVisibility(View.GONE); - return view; + return viewMain; } recyclerView.setAdapter(new ModulesAdapter(listModules(), (chk, position) -> { @@ -59,8 +109,282 @@ public abstract class BaseModuleFragment extends Fragment { listModules().get(position).deleteRemoveFile(); Snackbar.make(undeleteBtn, R.string.remove_file_deleted, Snackbar.LENGTH_SHORT).show(); })); - return view; + + + return viewMain; } + protected abstract List listModules(); + + public class ModulesAdapter extends RecyclerView.Adapter { + + private final List mList; + private final List mListToUpdate = new ArrayList<>(); + List mExpandedList; + @BindView(R.id.expand_layout) + LinearLayout expandedLayout; + private View viewMain; + private Context context; + private final Utils.ItemClickListener chboxListener; + private final Utils.ItemClickListener deleteBtnListener; + private final Utils.ItemClickListener unDeleteBtnListener; + private boolean alertUpdate, ignoreAlertUpdate; + + public ModulesAdapter(List list, Utils.ItemClickListener chboxListener, Utils.ItemClickListener deleteBtnListener, Utils.ItemClickListener undeleteBtnListener) { + alertUpdate = false; + this.mList = list; + mExpandedList = new ArrayList<>(mList.size()); + for (int i = 0; i < mList.size(); i++) { + mExpandedList.add(false); + if (listModules().get(i).isUpdateAvailable()) { + alertUpdate = true; + mListToUpdate.add(listModules().get(i)); + } + } + this.chboxListener = chboxListener; + this.deleteBtnListener = deleteBtnListener; + this.unDeleteBtnListener = undeleteBtnListener; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + viewMain = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_module, parent, false); + context = parent.getContext(); + ButterKnife.bind(this, viewMain); + return new ViewHolder(viewMain); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + final Module module = mList.get(position); + Log.d("Magisk", "ModulesAdapter: Trying set up bindview from list pos " + position + " and " + module.getName()); + Log.d("Magisk", "BaseModuleFragment: Log ID is " + module.getmLogUrl()); + holder.title.setText(module.getName()); + holder.versionName.setText(module.getVersion()); + holder.description.setText(module.getDescription()); + holder.author.setText(module.getAuthor()); + String logUrl = module.getmLogUrl(); + String supportUrl = module.getmSupportUrl(); + String donateUrl = module.getmDonateUrl(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + if (prefs.contains("ignoreUpdateAlerts")) { + ignoreAlertUpdate = prefs.getBoolean("ignoreUpdateAlerts", false); + } + if (prefs.contains("repo-canUpdate_" + module.getId())) { + if (prefs.getBoolean("repo-canUpdate_" + module.getId(), false)) { + holder.updateStatus.setText(R.string.module_update_available); + holder.updateStatus.setVisibility(View.VISIBLE); + } else { + holder.updateStatus.setVisibility(View.GONE); + } + + if (alertUpdate && !ignoreAlertUpdate) { + Iterator iterRepo = mListToUpdate.iterator(); + while (iterRepo.hasNext()) { + Module mModule = iterRepo.next(); + DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + Utils.DownloadReceiver receiver = new Utils.DownloadReceiver() { + @Override + public void task(File file) { + Log.d("Magisk", "Task firing"); + new Utils.FlashZIP(context, mModule.getId(), file.toString()).execute(); + } + }; + String filename = mModule.getId().replace(" ", "") + ".zip"; + Utils.downloadAndReceive(context, receiver, mModule.getmZipUrl(), filename); + + break; + + case DialogInterface.BUTTON_NEGATIVE: + ignoreAlertUpdate = true; + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean("ignoreUpdateAlerts", ignoreAlertUpdate); + editor.apply(); + break; + } + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setMessage("An update is available for " + mModule.getName() + ". Would you like to install it?").setPositiveButton("Yes", dialogClickListener) + .setNegativeButton("No", dialogClickListener).show(); + mListToUpdate.remove(mModule); + } + } + } + + View.OnClickListener oCl = view -> { + if (view.getId() == holder.changeLog.getId()) { + + new WebWindow("Changelog", module.getmLogUrl(), context); + } + if (view.getId() == holder.authorLink.getId()) { + new WebWindow("Donate", module.getmDonateUrl(), context); + } + if (view.getId() == holder.supportLink.getId()) { + new WebWindow("Support", module.getmSupportUrl(), context); + } + }; + + holder.authorLink.setOnClickListener(oCl); + holder.changeLog.setOnClickListener(oCl); + holder.supportLink.setOnClickListener(oCl); + holder.checkBox.setChecked(module.isEnabled()); + holder.checkBox.setOnCheckedChangeListener((compoundButton, b) -> chboxListener.onItemClick(compoundButton, holder.getAdapterPosition())); + + holder.delete.setOnClickListener(view -> { + if (module.willBeRemoved()) { + unDeleteBtnListener.onItemClick(holder.delete, holder.getAdapterPosition()); + } else { + deleteBtnListener.onItemClick(holder.delete, holder.getAdapterPosition()); + } + + updateDeleteButton(holder, module); + }); + + updateDeleteButton(holder, module); + } + + private void updateDeleteButton(ViewHolder holder, Module module) { + holder.warning.setVisibility(module.willBeRemoved() ? View.VISIBLE : View.GONE); + + if (module.willBeRemoved()) { + holder.delete.setImageResource(R.drawable.ic_undelete); + } else { + holder.delete.setImageResource(R.drawable.ic_delete); + } + } + + @Override + public int getItemCount() { + return mList.size(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.title) + TextView title; + + @BindView(R.id.version_name) + TextView versionName; + @BindView(R.id.description) + TextView description; + @BindView(R.id.warning) + TextView warning; + @BindView(R.id.checkbox) + CheckBox checkBox; + @BindView(R.id.author) + TextView author; + @BindView(R.id.updateStatus) + TextView updateStatus; + @BindView(R.id.delete) + ImageView delete; + @BindView(R.id.changeLog) + ImageView changeLog; + @BindView(R.id.authorLink) + ImageView authorLink; + @BindView(R.id.supportLink) + ImageView supportLink; + @BindView(R.id.expand_layout) + LinearLayout expandLayout; + private ValueAnimator mAnimator; + private int mMeasuredHeight; + + public ViewHolder(View itemView) { + super(itemView); + WindowManager windowmanager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + ButterKnife.bind(this, itemView); + DisplayMetrics dimension = new DisplayMetrics(); + windowmanager.getDefaultDisplay().getMetrics(dimension); + + expandLayout.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + + + @Override + public boolean onPreDraw() { + expandLayout.getViewTreeObserver().removeOnPreDrawListener(this); + expandLayout.setVisibility(View.GONE); + final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + expandLayout.measure(widthSpec, heightSpec); + mAnimator = slideAnimator(0, expandLayout.getMeasuredHeight()); + return true; + } + }); + + viewMain.setOnClickListener(view -> { + int position = getAdapterPosition(); + Log.d("Magisk", "ReposFragment: CLICK. " + position + " and " + mExpandedList.get(position)); + + if (mExpandedList.get(position)) { + collapse(expandLayout); + } else { + expand(expandLayout); + } + mExpandedList.set(position, !mExpandedList.get(position)); + + }); + if (!Shell.rootAccess()) { + checkBox.setEnabled(false); + delete.setEnabled(false); + } + } + + private void expand(View view) { + + // set Visible + + + Log.d("Magisk", "ReposFragment: Expand anim called " + mMeasuredHeight + " and " + view.getId()); + view.setVisibility(View.VISIBLE); + mAnimator.start(); + } + + private void collapse(View view) { + int finalHeight = view.getHeight(); + ValueAnimator mAnimator = slideAnimator(finalHeight, 0); + Log.d("Magisk", "ReposFragment: Collapse anim called " + finalHeight + " and " + view.getId()); + + mAnimator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationEnd(Animator animator) { + // Height=0, but it set visibility to GONE + view.setVisibility(View.GONE); + } + + @Override + public void onAnimationStart(Animator animator) { + } + + @Override + public void onAnimationCancel(Animator animator) { + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + }); + mAnimator.start(); + } + + private ValueAnimator slideAnimator(int start, int end) { + + ValueAnimator animator = ValueAnimator.ofInt(start, end); + + animator.addUpdateListener(valueAnimator -> { + // Update Height + int value = (Integer) valueAnimator.getAnimatedValue(); + + ViewGroup.LayoutParams layoutParams = expandLayout + .getLayoutParams(); + layoutParams.height = value; + expandLayout.setLayoutParams(layoutParams); + }); + return animator; + } + } + } } diff --git a/app/src/main/java/com/topjohnwu/magisk/LogFragment.java b/app/src/main/java/com/topjohnwu/magisk/LogFragment.java index 56abc16f2..27d232c6a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/LogFragment.java +++ b/app/src/main/java/com/topjohnwu/magisk/LogFragment.java @@ -25,17 +25,13 @@ import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; -import com.topjohnwu.magisk.utils.Shell; import com.topjohnwu.magisk.utils.Utils; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.util.Calendar; import java.util.List; -import java.util.concurrent.ExecutionException; import butterknife.BindView; import butterknife.ButterKnife; diff --git a/app/src/main/java/com/topjohnwu/magisk/ModulesAdapter.java b/app/src/main/java/com/topjohnwu/magisk/ModulesAdapter.java deleted file mode 100644 index f5cf729f1..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ModulesAdapter.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.topjohnwu.magisk; - -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.ImageView; -import android.widget.TextView; - -import com.topjohnwu.magisk.module.Module; -import com.topjohnwu.magisk.utils.Shell; -import com.topjohnwu.magisk.utils.Utils; - -import java.util.List; - -import butterknife.BindView; -import butterknife.ButterKnife; - -public class ModulesAdapter extends RecyclerView.Adapter { - - private final List mList; - private final Utils.ItemClickListener chboxListener; - private final Utils.ItemClickListener deleteBtnListener; - private final Utils.ItemClickListener unDeleteBtnListener; - - public ModulesAdapter(List list, Utils.ItemClickListener chboxListener, Utils.ItemClickListener deleteBtnListener, Utils.ItemClickListener undeleteBtnListener) { - this.mList = list; - this.chboxListener = chboxListener; - this.deleteBtnListener = deleteBtnListener; - this.unDeleteBtnListener = undeleteBtnListener; - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_module, parent, false); - - return new ViewHolder(view); - } - - @Override - public void onBindViewHolder(final ViewHolder holder, int position) { - final Module module = mList.get(position); - - holder.title.setText(module.getName()); - holder.versionName.setText(module.getVersion()); - holder.description.setText(module.getDescription()); - - holder.checkBox.setChecked(module.isEnabled()); - holder.checkBox.setOnCheckedChangeListener((compoundButton, b) -> chboxListener.onItemClick(compoundButton, holder.getAdapterPosition())); - - holder.delete.setOnClickListener(view -> { - if (module.willBeRemoved()) { - unDeleteBtnListener.onItemClick(holder.delete, holder.getAdapterPosition()); - } else { - deleteBtnListener.onItemClick(holder.delete, holder.getAdapterPosition()); - } - - updateDeleteButton(holder, module); - }); - - updateDeleteButton(holder, module); - } - - private void updateDeleteButton(ViewHolder holder, Module module) { - holder.warning.setVisibility(module.willBeRemoved() ? View.VISIBLE : View.GONE); - - if (module.willBeRemoved()) { - holder.delete.setImageResource(R.drawable.ic_undelete); - } else { - holder.delete.setImageResource(R.drawable.ic_delete); - } - } - - @Override - public int getItemCount() { - return mList.size(); - } - - static class ViewHolder extends RecyclerView.ViewHolder { - - @BindView(R.id.title) TextView title; - - @BindView(R.id.version_name) TextView versionName; - @BindView(R.id.description) TextView description; - - @BindView(R.id.warning) TextView warning; - - @BindView(R.id.checkbox) CheckBox checkBox; - @BindView(R.id.delete) ImageView delete; - - public ViewHolder(View itemView) { - super(itemView); - ButterKnife.bind(this, itemView); - if (!Shell.rootAccess()) { - checkBox.setEnabled(false); - delete.setEnabled(false); - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ModulesFragment.java b/app/src/main/java/com/topjohnwu/magisk/ModulesFragment.java index a67b47c6e..3ecfcc69b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ModulesFragment.java +++ b/app/src/main/java/com/topjohnwu/magisk/ModulesFragment.java @@ -2,9 +2,12 @@ package com.topjohnwu.magisk; import android.app.Activity; import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.provider.MediaStore; import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.TabLayout; @@ -12,98 +15,121 @@ import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; +import android.widget.Toast; +import com.ipaulpro.afilechooser.FileInfo; +import com.ipaulpro.afilechooser.utils.FileUtils; import com.topjohnwu.magisk.module.Module; -import com.topjohnwu.magisk.utils.Shell; +import com.topjohnwu.magisk.module.RepoHelper; import com.topjohnwu.magisk.utils.Utils; -import java.io.File; -import java.io.FileNotFoundException; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; -import java.util.concurrent.ExecutionException; import butterknife.BindView; import butterknife.ButterKnife; public class ModulesFragment extends Fragment { + private static final int FETCH_ZIP_CODE = 2; public static List listModules = new ArrayList<>(); public static List listModulesCache = new ArrayList<>(); - private static final int FILE_SELECT_CODE = 0; - private File input; - - @BindView(R.id.progressBar) ProgressBar progressBar; - @BindView(R.id.fab) FloatingActionButton fabio; - @BindView(R.id.pager) ViewPager viewPager; - @BindView(R.id.tab_layout) TabLayout tabLayout; + @BindView(R.id.progressBar) + ProgressBar progressBar; + @BindView(R.id.fab) + FloatingActionButton fabio; + @BindView(R.id.pager) + ViewPager viewPager; + @BindView(R.id.tab_layout) + TabLayout tabLayout; + private int viewPagePosition; + private RepoHelper.TaskDelegate mTaskDelegate; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.modules_fragment, container, false); + ButterKnife.bind(this, view); + fabio.setOnClickListener(v -> { + Intent getContentIntent = FileUtils.createGetContentIntent(null); + getContentIntent.setType("application/zip"); + Intent fileIntent = Intent.createChooser(getContentIntent, "Select a file"); + + startActivityForResult(fileIntent, FETCH_ZIP_CODE); + + }); + + new Utils.LoadModules(getActivity(), false).execute(); + mTaskDelegate = result -> { + if (result.equals("OK")) { + RefreshUI(); + } + + }; new updateUI().execute(); - - setHasOptionsMenu(true); return view; } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (data != null) { + // Get the URI of the selected file + final Uri uri = data.getData(); + Log.i("Magisk", "ModulesFragment: Uri = " + uri.toString() + " or "); + new Utils.FlashZIP(getActivity(),uri).execute(); + try { + // Get the file path from the URI + FileInfo fileInfo = FileUtils.getFileInfo(getActivity(), uri); + Toast.makeText(getActivity(), + "File Selected: " + fileInfo.getDisplayName() + " size: " + fileInfo.getSize(), Toast.LENGTH_LONG).show(); + + if (!fileInfo.isExternal()) { + + } else { + + } + } catch (Exception e) { + Log.e("FileSelectorTestAc...", "File select error", e); + } + } + + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_module, menu); - fabio.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - Intent intent = new Intent(); - intent.setType("*/zip"); - intent.setAction(Intent.ACTION_GET_CONTENT); - startActivityForResult(intent,FILE_SELECT_CODE); - } - - - }); } - public void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case FILE_SELECT_CODE: - if (resultCode == Activity.RESULT_OK) { - // Get the Uri of the selected file - Uri uri = data.getData(); - String path = uri.getPath(); - String fileName = uri.getLastPathSegment(); - new Utils.FlashZIP(getActivity(), fileName, path).execute(); + private void RefreshUI() { + viewPagePosition = tabLayout.getSelectedTabPosition(); + listModules.clear(); + listModulesCache.clear(); + progressBar.setVisibility(View.VISIBLE); + viewPager.setAdapter(new TabsAdapter(getChildFragmentManager())); + tabLayout.setupWithViewPager(viewPager); + viewPager.setCurrentItem(viewPagePosition); + new Utils.LoadModules(getActivity(), true).execute(); + Collections.sort(listModules, new CustomComparator()); + Collections.sort(listModulesCache, new CustomComparator()); + new updateUI().execute(); + } - } - break; - }} - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.force_reload: - listModules.clear(); - listModulesCache.clear(); - progressBar.setVisibility(View.VISIBLE); - viewPager.setAdapter(new TabsAdapter(getChildFragmentManager())); - tabLayout.setupWithViewPager(viewPager); - new Utils.LoadModules(getContext()).execute(); - new updateUI().execute(); - break; - } - - return super.onOptionsItemSelected(item); + void selectPage(int pageIndex) { + tabLayout.setScrollPosition(pageIndex, 0f, true); + viewPager.setCurrentItem(pageIndex); } public static class NormalModuleFragment extends BaseModuleFragment { @@ -134,11 +160,11 @@ public class ModulesFragment extends Fragment { @Override protected void onPostExecute(Void v) { super.onPostExecute(v); - progressBar.setVisibility(View.GONE); - viewPager.setAdapter(new TabsAdapter(getChildFragmentManager())); tabLayout.setupWithViewPager(viewPager); + selectPage(viewPagePosition); + } } @@ -165,10 +191,22 @@ public class ModulesFragment extends Fragment { @Override public Fragment getItem(int position) { if (position == 0) { - return new NormalModuleFragment(); + NormalModuleFragment nmf = new NormalModuleFragment(); + nmf.SetDelegate(mTaskDelegate); + return nmf; } else { - return new CacheModuleFragment(); + CacheModuleFragment cmf = new CacheModuleFragment(); + cmf.SetDelegate(mTaskDelegate); + return cmf; } } } -} + + public class CustomComparator implements Comparator { + @Override + public int compare(Module o1, Module o2) { + return o1.getName().compareTo(o2.getName()); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ReposAdapter.java b/app/src/main/java/com/topjohnwu/magisk/ReposAdapter.java new file mode 100644 index 000000000..26f136e66 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ReposAdapter.java @@ -0,0 +1,312 @@ +package com.topjohnwu.magisk; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.preference.PreferenceManager; +import android.support.v7.widget.RecyclerView; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.topjohnwu.magisk.module.Repo; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.magisk.utils.WebWindow; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class ReposAdapter extends RecyclerView.Adapter { + + private final List mList; + List mExpandedList; + private View viewMain; + private Context context; + private boolean mCanUpdate; + private boolean alertUpdate; + private boolean ignoreAlertUpdate; + private Repo repo; + private ViewHolder mHolder; + private String mDonateUrl, mSupportUrl, mLogUrl,alertPackage; + private SharedPreferences prefs; + + + public ReposAdapter(ReposFragment reposFragment, List list) { + ReposFragment reposFragment1 = reposFragment; + alertPackage = ""; + alertUpdate = false; + this.mList = list; + Log.d("Magisk", "ReposAdapter: I am alive. I have a list " + list.size()); + mExpandedList = new ArrayList<>(mList.size()); + for (int i = 0; i < mList.size(); i++) { + mExpandedList.add(false); + if (mList.get(i).canUpdate()) { + alertUpdate = true; + if (alertPackage.equals("")) { + alertPackage = mList.get(i).getName(); + } else { + alertPackage += mList.get(i).getName() + ", "; + } + } + } + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + viewMain = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_repo, parent, false); + ButterKnife.bind(this, viewMain); + context = parent.getContext(); + return new ViewHolder(viewMain); + } + +// @Override +// public boolean onOptionsItemSelected(MenuItem item) { +// switch (item.getItemId()) { +// case R.id.force_reload: +// listModulesDownload.clear(); +// new Utils.LoadModules(getActivity(), true).execute(); +// break; +// } +// +// return super.onOptionsItemSelected(item); +// } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + prefs = PreferenceManager.getDefaultSharedPreferences(context); + repo = mList.get(position); + mHolder = holder; + mDonateUrl = repo.getmDonateUrl(); + mSupportUrl = repo.getmSupportUrl(); + mLogUrl = repo.getmLogUrl(); + mExpandedList = new ArrayList<>(mList.size()); + for (int i = 0; i < mList.size(); i++) { + mExpandedList.add(false); + } + SetupViewElements(repo); + + } + + private void SetupViewElements(Repo repo) { + int mPosition = mHolder.getAdapterPosition(); + String titleString; + if (repo.getId() != null) { + if (repo.isCacheModule()) { + titleString = "[Cache] " + repo.getName(); + } else { + titleString = repo.getName(); + } + + mHolder.title.setText(titleString); + mHolder.versionName.setText(repo.getmVersion()); + mHolder.description.setText(repo.getDescription()); + String authorString = this.context.getResources().getString(R.string.author) + " " + repo.getmAuthor(); + mHolder.author.setText(authorString); + String logUrl = repo.getmLogUrl(); + String supportUrl = repo.getmSupportUrl(); + String donateUrl = repo.getmDonateUrl(); + if (supportUrl.equals("")) mHolder.supportLink.setBackgroundColor(Color.GRAY); + if (logUrl.equals("")) mHolder.changeLog.setBackgroundColor(Color.GRAY); + if (donateUrl.equals("")) mHolder.authorLink.setBackgroundColor(Color.GRAY); + if (prefs.contains("ignoreUpdateAlerts")) { + ignoreAlertUpdate = prefs.getBoolean("ignoreUpdateAlerts",false); + } + mHolder.installedStatus.setText(repo.isInstalled() ? this.context.getResources().getString(R.string.module_installed) : this.context.getResources().getString(R.string.module_not_installed)); + if (mExpandedList.get(mPosition)) { + mHolder.expandLayout.setVisibility(View.VISIBLE); + } else { + mHolder.expandLayout.setVisibility(View.GONE); + } + + if (repo.isInstalled()) { + mHolder.installedStatus.setTextColor(Color.parseColor("#14AD00")); + mHolder.updateStatus.setText(repo.canUpdate() ? this.context.getResources().getString(R.string.module_update_available) : this.context.getResources().getString(R.string.module_up_to_date)); + } + + Log.d("Magisk", "ReposAdapter: Setting up info " + repo.getId() + " and " + repo.getDescription() + " and " + repo.getmVersion()); + prefs = PreferenceManager.getDefaultSharedPreferences(context); + mCanUpdate = prefs.getBoolean("repo-canUpdate_" + repo.getId(), false); + + + View.OnClickListener oCl = view -> { + Log.d("Magisk", "Onlick captured, view is " + view.getId()); + + if (view.getId() == mHolder.updateImage.getId()) { + if (!repo.isInstalled() | repo.canUpdate()) { + + Utils.DownloadReceiver receiver = new Utils.DownloadReceiver() { + @Override + public void task(File file) { + Log.d("Magisk", "Task firing"); + new Utils.FlashZIP(context, repo.getId(), file.toString()).execute(); + } + }; + String filename = repo.getId().replace(" ", "") + ".zip"; + Utils.downloadAndReceive(context, receiver, repo.getmZipUrl(), filename); + } else { + Toast.makeText(context, repo.getId() + " is already installed.", Toast.LENGTH_SHORT).show(); + } + } + if ((view.getId() == mHolder.changeLog.getId()) && (!repo.getmLogUrl().equals(""))) { + new WebWindow("Changelog", repo.getmLogUrl(),context); + } + if ((view.getId() == mHolder.authorLink.getId()) && (!repo.getmSupportUrl().equals(""))) { + new WebWindow("Donate", repo.getmDonateUrl(),context); + } + if ((view.getId() == mHolder.supportLink.getId()) && (!repo.getmSupportUrl().equals(""))) { + new WebWindow("Support", repo.getmSupportUrl(),context); + } + }; + mHolder.changeLog.setOnClickListener(oCl); + mHolder.updateImage.setOnClickListener(oCl); + mHolder.authorLink.setOnClickListener(oCl); + mHolder.supportLink.setOnClickListener(oCl); + if (prefs.contains("repo-isInstalled_" + repo.getId())) { + boolean mIsInstalled = prefs.getBoolean("repo-isInstalled_" + repo.getId(), false); + + } + + } + } + + + + @Override + public int getItemCount() { + return mList.size(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.title) + TextView title; + @BindView(R.id.version_name) + TextView versionName; + @BindView(R.id.description) + TextView description; + @BindView(R.id.author) + TextView author; + @BindView(R.id.installedStatus) + TextView installedStatus; + @BindView(R.id.updateStatus) + TextView updateStatus; + @BindView(R.id.expand_layout) + LinearLayout expandLayout; + @BindView(R.id.update) + ImageView updateImage; + @BindView(R.id.installed) + ImageView installedImage; + @BindView(R.id.changeLog) + ImageView changeLog; + @BindView(R.id.authorLink) + ImageView authorLink; + @BindView(R.id.supportLink) + ImageView supportLink; + private ValueAnimator mAnimator; + private ObjectAnimator animY2; + private ViewHolder holder; + + public ViewHolder(View itemView) { + super(itemView); + WindowManager windowmanager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + ButterKnife.bind(this, itemView); + DisplayMetrics dimension = new DisplayMetrics(); + windowmanager.getDefaultDisplay().getMetrics(dimension); + holder = this; + this.expandLayout.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + + @Override + public boolean onPreDraw() { + final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + holder.expandLayout.getViewTreeObserver().removeOnPreDrawListener(this); + holder.expandLayout.setVisibility(View.GONE); + holder.expandLayout.measure(widthSpec, heightSpec); + final int holderHeight = holder.expandLayout.getMeasuredHeight(); + mAnimator = slideAnimator(0, holderHeight); + animY2 = ObjectAnimator.ofFloat(holder.updateImage, "translationY", holderHeight / 2); + + return true; + } + + }); + + viewMain.setOnClickListener(view -> { + int position = getAdapterPosition(); + if (mExpandedList.get(position)) { + collapse(holder.expandLayout); + } else { + expand(holder.expandLayout); + } + mExpandedList.set(position, !mExpandedList.get(position)); + + }); + + } + + private void expand(View view) { + view.setVisibility(View.VISIBLE); + mAnimator.start(); + animY2.start(); + + } + + private void collapse(View view) { + int finalHeight = view.getHeight(); + ValueAnimator mAnimator = slideAnimator(finalHeight, 0); + mAnimator.addListener(new Animator.AnimatorListener() { + + @Override + public void onAnimationEnd(Animator animator) { + view.setVisibility(View.GONE); + } + + @Override + public void onAnimationStart(Animator animator) { + } + + @Override + public void onAnimationCancel(Animator animator) { + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + }); + mAnimator.start(); + animY2.reverse(); + + } + + private ValueAnimator slideAnimator(int start, int end) { + + ValueAnimator animator = ValueAnimator.ofInt(start, end); + + animator.addUpdateListener(valueAnimator -> { + int value = (Integer) valueAnimator.getAnimatedValue(); + ViewGroup.LayoutParams layoutParams = expandLayout + .getLayoutParams(); + layoutParams.height = value; + expandLayout.setLayoutParams(layoutParams); + }); + return animator; + } + + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ReposFragment.java b/app/src/main/java/com/topjohnwu/magisk/ReposFragment.java new file mode 100644 index 000000000..c4c6b8a64 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ReposFragment.java @@ -0,0 +1,204 @@ +package com.topjohnwu.magisk; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.topjohnwu.magisk.module.Repo; +import com.topjohnwu.magisk.module.RepoHelper; +import com.topjohnwu.magisk.utils.Utils; + +import java.io.File; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class ReposFragment extends Fragment { + + public static List mListRepos = new ArrayList<>(); + public static List mListReposToUpdate = new ArrayList<>(); + @BindView(R.id.recyclerView) + RecyclerView recyclerView; + @BindView(R.id.empty_rv) + TextView emptyTv; + @BindView(R.id.swipeRefreshLayout) + SwipeRefreshLayout swipeRefreshLayout; + private View mView; + private boolean mCanUpdate; + private boolean alertUpdate; + private boolean ignoreAlertUpdate; + private String alertPackage; + private SharedPreferences prefs; + + @Nullable + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.single_repo_fragment, container, false); + mView = view; + ButterKnife.bind(this, view); + prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + if (prefs.contains("ignoreUpdateAlerts")) { + ignoreAlertUpdate = prefs.getBoolean("ignoreUpdateAlerts", false); + } + swipeRefreshLayout.setOnRefreshListener(() -> { + this.LoadRepo(true); + ignoreAlertUpdate = false; + prefs.edit().putBoolean("ignoreUpdateAlerts",false).apply(); + + }); + LoadRepo(false); + setHasOptionsMenu(false); + alertUpdate = false; + if (mListRepos.size() == 0) { + emptyTv.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + return view; + } + CheckForUpdates(); + Log.d("Magisk", "ReposFragment: ListRepos size is " + listRepos().size()); + recyclerView.setAdapter(new ReposAdapter(this, mListRepos)); + return view; + } + + @Override + public void onStart() { + super.onStart(); + NotifyOfAlerts(); + + } + + private void CheckForUpdates() { + for (int i = 0; i < mListRepos.size(); i++) { + if (mListRepos.get(i).canUpdate()) { + alertUpdate = true; + mListReposToUpdate.add(mListRepos.get(i)); + + } + } + } + + @Override + public void onAttachFragment(Fragment childFragment) { + super.onAttachFragment(childFragment); + } + + private void LoadRepo (boolean doReload) { + RepoHelper.TaskDelegate taskDelegate = result -> { + if (result.equals("Complete")) { + Log.d("Magisk", "ReposFragment, got delegate"); + UpdateUI(); + if (mView != null) { + mView.invalidate(); + mView.requestLayout(); + } + + } + + }; + Log.d("Magisk","ReposFragment, LoadRepo called"); + mListRepos.clear(); + RepoHelper mr = new RepoHelper(); + List magiskRepos = mr.listRepos(getActivity(), doReload, taskDelegate); + + for (Repo repo : magiskRepos) { + Log.d("Magisk", "ReposFragment: Adding repo from string " + repo.getId()); + mListRepos.add(repo); + } + + } + + private void NotifyOfAlerts() { + if (alertUpdate && !ignoreAlertUpdate) { + Iterator iterRepo = mListReposToUpdate.iterator(); + while (iterRepo.hasNext()) { + Repo repo = iterRepo.next(); + DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + Utils.DownloadReceiver receiver = new Utils.DownloadReceiver() { + @Override + public void task(File file) { + Log.d("Magisk", "Task firing"); + new Utils.FlashZIP(getActivity(), repo.getId(), file.toString()).execute(); + } + }; + String filename = repo.getId().replace(" ", "") + ".zip"; + Utils.downloadAndReceive(getActivity(), receiver, repo.getmZipUrl(), filename); + + break; + + case DialogInterface.BUTTON_NEGATIVE: + ignoreAlertUpdate = true; + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean("ignoreUpdateAlerts", ignoreAlertUpdate); + editor.apply(); + break; + } + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage("An update is available for " + repo.getName() + ". Would you like to install it?").setPositiveButton("Yes", dialogClickListener) + .setNegativeButton("No", dialogClickListener).show(); + iterRepo.remove(); + } + + } + } + + + @Override + public void onResume() { + super.onResume(); + LoadRepo(false); + + } + + + + protected List listRepos() { + return mListRepos; + } + + private void UpdateUI() { + Log.d("Magisk","ReposFragment: UpdateUI Called, size is " + listRepos().size()); + + if (listRepos().size() == 0) { + emptyTv.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + + } + Log.d("Magisk", "ReposFragment: ListRepos size is " + listRepos().size()); + recyclerView.setAdapter(new ReposAdapter(this, listRepos())); + if (swipeRefreshLayout.isRefreshing()) { + swipeRefreshLayout.setRefreshing(false); + CheckForUpdates(); + NotifyOfAlerts(); + } + + + } + + + + + + + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/WelcomeActivity.java b/app/src/main/java/com/topjohnwu/magisk/WelcomeActivity.java index 8125977da..f45b2d8aa 100644 --- a/app/src/main/java/com/topjohnwu/magisk/WelcomeActivity.java +++ b/app/src/main/java/com/topjohnwu/magisk/WelcomeActivity.java @@ -1,16 +1,14 @@ package com.topjohnwu.magisk; import android.Manifest; -import android.app.ActivityManager; -import android.app.AppOpsManager; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; -import android.provider.Settings; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; @@ -25,6 +23,7 @@ import android.support.v7.widget.Toolbar; import android.view.MenuItem; import android.view.View; +import com.topjohnwu.magisk.module.RepoHelper; import com.topjohnwu.magisk.utils.Utils; import butterknife.BindView; @@ -33,15 +32,13 @@ import butterknife.ButterKnife; public class WelcomeActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { private static final String SELECTED_ITEM_ID = "SELECTED_ITEM_ID"; + private Context mContext; private final Handler mDrawerHandler = new Handler(); - @BindView(R.id.toolbar) - Toolbar toolbar; - @BindView(R.id.drawer_layout) - DrawerLayout drawer; - @BindView(R.id.nav_view) - NavigationView navigationView; + @BindView(R.id.toolbar) Toolbar toolbar; + @BindView(R.id.drawer_layout) DrawerLayout drawer; + @BindView(R.id.nav_view) NavigationView navigationView; @IdRes private int mSelectedId = R.id.magisk; @@ -51,28 +48,34 @@ public class WelcomeActivity extends AppCompatActivity implements NavigationView super.onCreate(savedInstanceState); setContentView(R.layout.activity_welcome); ButterKnife.bind(this); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); } - // Startups - PreferenceManager.setDefaultValues(this, R.xml.defaultpref, false); - if (!isMyServiceRunning(MonitorService.class)) { - Intent myIntent = new Intent(getApplication(), MonitorService.class); - getApplication().startService(myIntent); - } if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0); } - if (!hasPermission()) { - startActivityForResult(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS), 100); - } - new Utils.Initialize(this).execute(); new Utils.CheckUpdates(this).execute(); - new Utils.LoadModules(this).execute(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + RepoHelper.TaskDelegate delegate = result -> { + //Do a thing here when we get a result we want + }; + if (!prefs.contains("oauth_key")) { + + } + if (!prefs.contains("hasCachedRepos")) { + new Utils.LoadModules(this, true).execute(); + new Utils.LoadRepos(this, true,delegate).execute(); + + } else { + new Utils.LoadModules(getApplication(),false).execute(); + new Utils.LoadRepos(this, false,delegate).execute(); + + } setSupportActionBar(toolbar); @@ -102,12 +105,10 @@ public class WelcomeActivity extends AppCompatActivity implements NavigationView } navigationView.setNavigationItemSelectedListener(this); - if (getIntent().hasExtra("relaunch")) { - navigate(R.id.root); - } } + @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -134,25 +135,6 @@ public class WelcomeActivity extends AppCompatActivity implements NavigationView return true; } - private boolean hasPermission() { - AppOpsManager appOps = (AppOpsManager) - getSystemService(Context.APP_OPS_SERVICE); - int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, - android.os.Process.myUid(), getPackageName()); - return mode == AppOpsManager.MODE_ALLOWED; - - } - - private boolean isMyServiceRunning(Class serviceClass) { - ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); - for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { - if (serviceClass.getName().equals(service.service.getClassName())) { - return true; - } - } - return false; - } - private void navigate(final int itemId) { Fragment navFragment = null; String tag = ""; @@ -167,16 +149,16 @@ public class WelcomeActivity extends AppCompatActivity implements NavigationView tag = "root"; navFragment = new RootFragment(); break; - case R.id.autoroot: - setTitle(R.string.auto_root); - tag = "ic_autoroot"; - navFragment = new AutoRootFragment(); - break; case R.id.modules: setTitle(R.string.modules); tag = "modules"; navFragment = new ModulesFragment(); break; + case R.id.downloads: + setTitle(R.string.downloads); + tag = "downloads"; + navFragment = new ReposFragment(); + break; case R.id.log: setTitle(R.string.log); tag = "log"; @@ -198,6 +180,4 @@ public class WelcomeActivity extends AppCompatActivity implements NavigationView } } } - - -} +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/module/Module.java b/app/src/main/java/com/topjohnwu/magisk/module/Module.java index 74afbe3ab..f052971be 100644 --- a/app/src/main/java/com/topjohnwu/magisk/module/Module.java +++ b/app/src/main/java/com/topjohnwu/magisk/module/Module.java @@ -1,14 +1,12 @@ package com.topjohnwu.magisk.module; +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; import android.util.Log; -import com.topjohnwu.magisk.R; import com.topjohnwu.magisk.utils.Utils; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; - public class Module { private String mRemoveFile; @@ -17,50 +15,143 @@ public class Module { private String mName = null; private String mVersion = "(No version provided)"; private String mDescription = "(No description provided)"; + private String mUrl,mSupportUrl,mDonateUrl,mZipUrl,mBaseUrl,mManifestUrl,mAuthor,mLogUrl; + private boolean mEnable, mRemove,mUpdateAvailable, mIsInstalled,mIsCacheModule; - private boolean mEnable; - private boolean mRemove; private String mId; private int mVersionCode; + public Module(String path, Context context) { - public Module(String path) { mRemoveFile = path + "/remove"; mDisableFile = path + "/disable"; for (String line : Utils.readFile(path + "/module.prop")) { - String[] parts = line.split("=", 2); - if (parts.length != 2) { + String[] props = line.split("=", 2); + if (props.length != 2) { continue; } - String key = parts[0].trim(); + String key = props[0].trim(); if (key.charAt(0) == '#') { continue; } - String value = parts[1].trim(); - switch (key) { + switch (props[0]) { + case "versionCode": + this.mVersionCode = Integer.valueOf(props[1]); + break; case "name": - mName = value; + this.mName = props[1]; break; - case "version": - mVersion = value; - break; - case "description": - mDescription = value; + case "author": + this.mAuthor = props[1]; break; case "id": - mId = value; + this.mId = props[1]; break; - case "versionCode": - try { - mVersionCode = Integer.parseInt(value); - } catch (NumberFormatException e) { - mVersionCode = 0; - } + case "version": + this.mVersion = props[1]; + break; + case "description": + this.mDescription = props[1]; + break; + case "donate": + this.mDonateUrl = props[1]; + break; + case "cacheModule": + this.mIsCacheModule = Boolean.valueOf(props[1]); + break; + case "support": + this.mSupportUrl = props[1]; + break; + case "donateUrl": + this.mDonateUrl = props[1]; + break; + case "zipUrl": + this.mZipUrl = props[1]; + break; + case "baseUrl": + this.mBaseUrl = props[1]; + break; + case "manifestUrl": + this.mManifestUrl = props[1]; + break; + case "logUrl": + this.mLogUrl = props[1]; + break; + default: + Log.d("Magisk", "Module: Manifest string not recognized: " + props[0]); break; } + + + } + Log.d("Magisk","Module: Loaded module with ID of " + this.mId + " or " + mId); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + if (this.mId != null && !this.mId.isEmpty()) { + String preferenceString = "repo_" + this.mId; + String preferenceKey = prefs.getString(preferenceString,"nope"); + Log.d("Magisk", "Module: Checking for preference named " + preferenceString); + if (!preferenceKey.equals("nope")) { + Log.d("Magisk", "Module: repo_" + mId + " found."); + String entryString = prefs.getString("repo_" + mId, ""); + String[] subStrings = entryString.split("\n"); + for (String subKeys : subStrings) { + String[] idEntry = subKeys.split("=", 2); + if (idEntry[0].equals("id")) { + if (idEntry.length != 2) { + continue; + } + + if (idEntry[1].equals(mId)) { + Log.d("Magisk", "Module: Hey, I know " + mId + " is online..."); + mIsInstalled = true; + } else mIsInstalled = false; + } + if (idEntry[0].equals("logUrl")) { + mLogUrl = idEntry[1]; + } + if (idEntry[0].equals("support")) { + mSupportUrl = idEntry[1]; + } + if (idEntry[0].equals("zipUrl")) { + mZipUrl = idEntry[1]; + } + if (idEntry[0].equals("donate")) { + mDonateUrl = idEntry[1]; + } + + if (idEntry[0].equals("versionCode")) { + if (idEntry.length != 2) { + continue; + } + + if (Integer.valueOf(idEntry[1]) > mVersionCode) { + mUpdateAvailable = true; + Log.d("Magisk", "Module: Hey, I have an update..."); + } else mUpdateAvailable = false; + } + } + + + } + + SharedPreferences.Editor editor = prefs.edit(); + if (mIsInstalled) { + editor.putBoolean("repo-isInstalled_" + mId, true); + } else { + editor.putBoolean("repo-isInstalled_" + mId, false); + } + + if (mUpdateAvailable) { + editor.putBoolean("repo-canUpdate_" + mId, true); + } else { + editor.putBoolean("repo-canUpdate_" + mId, false); + } + editor.apply(); } if (mName == null) { @@ -74,6 +165,21 @@ public class Module { } + public Module(Repo repo) { + + mName = repo.getName(); + mVersion = repo.getmVersion(); + mDescription = repo.getDescription(); + mId = repo.getId(); + mVersionCode = repo.getmVersionCode(); + mUrl = repo.getmZipUrl(); + mEnable = true; + mRemove = false; + + } + + + public String getName() { return mName; } @@ -82,6 +188,14 @@ public class Module { return mVersion; } + public String getAuthor() { + return mAuthor; + } + + public String getId() {return mId; } + + public String getmLogUrl() {return mLogUrl; } + public String getDescription() { return mDescription; } @@ -110,4 +224,22 @@ public class Module { return mRemove; } + public String getmDonateUrl() { + return mDonateUrl; + } + + public String getmZipUrl() { return mZipUrl; } + + public String getmManifestUrl() { + return mManifestUrl; + } + + public String getmSupportUrl() { + return mSupportUrl; + } + + public boolean isInstalled() {return mIsInstalled; } + + public boolean isUpdateAvailable() { return mUpdateAvailable; } + } \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/module/Repo.java b/app/src/main/java/com/topjohnwu/magisk/module/Repo.java new file mode 100644 index 000000000..23c74dcdf --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/module/Repo.java @@ -0,0 +1,274 @@ +package com.topjohnwu.magisk.module; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.magisk.utils.WebRequest; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Date; + +public class Repo { + private String mBaseUrl; + private String mZipUrl; + private String mLogUrl; + private String mManifestUrl; + private String mVersion; + private String mName; + private String mDescription; + private String mAuthor; + public String mAuthorUrl; + private String mId; + private String mVersionCode; + private String mSupportUrl; + private String mDonateUrl; + private String lastUpdate; + private Context appContext; + private boolean mIsInstalled,mCanUpdate,mIsCacheModule; + + + public Repo(String manifestString, Context context) { + appContext = context; + ParseProps(manifestString); + + } + + + + public Repo(String name, String url, Date updated, Context context) { + appContext = context; + this.mName = name; + this.mBaseUrl = url; + this.lastUpdate = updated.toString(); + + this.fetch(); + + } + + public Repo(String moduleName, String moduleDescription, String zipUrl, Date lastUpdated, Context context) { + appContext = context; + this.mZipUrl = zipUrl; + this.mDescription = moduleDescription; + this.mName = moduleName; + this.lastUpdate = lastUpdated.toString(); + this.fetch(); + + } + + public void fetch() { + + WebRequest webreq = new WebRequest(); + // Construct initial url for contents + String repoString = webreq.makeWebServiceCall(mBaseUrl + "/contents?access_token=" + Utils.procFile(appContext.getString(R.string.some_string),appContext), WebRequest.GET); + try { + JSONArray repoArray = new JSONArray(repoString); + for (int f = 0; f < repoArray.length(); f++) { + JSONObject jsonobject = repoArray.getJSONObject(f); + String name = jsonobject.getString("name"); + String url = jsonobject.getString("download_url").trim(); + Log.d("Magisk","Repo - checking object named " + name + " with value of " + url); + if (name.contains(".zip")) { + this.mZipUrl = url; + } + if (name.equals("module.prop")) { + this.mManifestUrl = url; + } + if (name.contains("log.txt")) { + Log.d("Magisk","Repo: Setting log URL for " + name + " of " + url); + this.mLogUrl = url; + } + } + } catch (JSONException e) { + e.printStackTrace(); + } + + WebRequest propReq = new WebRequest(); + String manifestString = propReq.makeWebServiceCall(mManifestUrl,WebRequest.GET,true); + + if (ParseProps(manifestString)) { + PutProps(manifestString); + } + + } + + private void PutProps(String manifestString) { + manifestString = manifestString + "zipUrl=" + mZipUrl + "\nbaseUrl=" + mBaseUrl + "\nlogUrl=" + mLogUrl + "\nmanifestUrl=" + mManifestUrl; + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString("repo_" + mId, manifestString); + editor.putBoolean("hasCachedRepos", true); + editor.putString("updated_" + mId, this.lastUpdate); + editor.apply(); + } + private boolean ParseProps(String string) { + + if ((string.length() <= 1) | (!string.contains("id"))) { + return false; + } else { + String lines[] = string.split("\\n"); + for (String line : lines) { + if (line != "") { + String props[] = line.split("="); + switch (props[0]) { + case "versionCode": + this.mVersionCode = props[1]; + break; + case "name": + this.mName = props[1]; + break; + case "author": + this.mAuthor = props[1]; + break; + case "id": + this.mId = props[1]; + break; + case "version": + this.mVersion = props[1]; + break; + case "description": + this.mDescription = props[1]; + break; + case "donate": + this.mDonateUrl = props[1]; + break; + case "cacheModule": + this.mIsCacheModule = Boolean.valueOf(props[1]); + break; + case "support": + this.mSupportUrl = props[1]; + break; + case "logUrl": + this.mLogUrl = props[1]; + break; + case "donateUrl": + this.mDonateUrl = props[1]; + break; + case "zipUrl": + this.mZipUrl = props[1]; + break; + case "baseUrl": + this.mBaseUrl = props[1]; + break; + case "manifestUrl": + this.mManifestUrl = props[1]; + break; + default: + Log.d("Magisk", "Manifest string not recognized: " + props[0]); + break; + } + } + + } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext); + if (prefs.contains("repo-isInstalled_" + this.mId)) { + mIsInstalled = prefs.getBoolean("repo-isInstalled_" + this.mId,false); + } + if (prefs.contains("repo-canUpdate_" + this.mId)) { + mCanUpdate = prefs.getBoolean("repo-canUpdate_" + this.mId,false); + } + if (prefs.contains("updated_" + this.mId)) { + lastUpdate = prefs.getString("updated_" + this.mId,""); + } + + + + return this.mId != null; + + } + } + + + + + + + + + public String getStringProperty(String mValue) { + switch (mValue) { + case "author": + return mAuthor; + case "id": + return mId; + case "version": + return mVersion; + case "description": + return mDescription; + case "supportUrl": + return mSupportUrl; + case "donateUrl": + return mDonateUrl; + case "baseeUrl": + return mBaseUrl; + case "zipUrl": + return mZipUrl; + default: + return null; + } + } + + public String getName() { + return mName; + } + + public String getmVersion() { + return mVersion; + } + + public int getmVersionCode() { + return Integer.valueOf(mVersionCode); + } + + public String getDescription() { + return mDescription; + } + + public String getId() { + return mId; + } + + public String getmZipUrl() { + return mZipUrl; + } + + public String getmBaseUrl() { + return mBaseUrl; + } + + public String getmLogUrl() { + return mLogUrl; + } + + + public String getmAuthor() { + return mAuthor; + } + + public String getmDonateUrl() { + return mDonateUrl; + } + + public String getmManifestUrl() { + return mManifestUrl; + } + + public String getmSupportUrl() { + return mSupportUrl; + } + + public String getLastUpdate() { + return lastUpdate; + } + + public boolean isInstalled() { return mIsInstalled; } + public boolean canUpdate() { return mCanUpdate; } + public boolean isCacheModule() { return mIsCacheModule; } +} + diff --git a/app/src/main/java/com/topjohnwu/magisk/module/RepoHelper.java b/app/src/main/java/com/topjohnwu/magisk/module/RepoHelper.java new file mode 100644 index 000000000..f360be9cd --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/module/RepoHelper.java @@ -0,0 +1,199 @@ +package com.topjohnwu.magisk.module; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.util.Log; +import android.widget.Toast; + +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.magisk.utils.WebRequest; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class RepoHelper { + private static List repos = new ArrayList<>(); + private static String TAG = "Magisk"; + private Context activityContext; + private Date updatedDate; + private SharedPreferences prefs; + private boolean apiFail; + + public RepoHelper() { + } + + public List listRepos(Context context, boolean refresh, TaskDelegate delegate) { + prefs = PreferenceManager.getDefaultSharedPreferences(context); + activityContext = context; + + if (!prefs.contains("hasCachedRepos") | refresh) { + Log.d(TAG, "RepoHelper: Building from web"); + new BuildFromWeb(delegate).execute(); + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + String date = format.format(Calendar.getInstance().getTime()); + prefs.edit().putString("last_update",date).apply(); + } else { + Log.d(TAG, "RepoHelper: Building from cache"); + BuildFromCache(); + } + + Collections.sort(repos, new CustomComparator()); + return repos; + } + + private void BuildFromCache() { + repos.clear(); + Map map = prefs.getAll(); + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey().contains("repo_")) { + String repoString = entry.getValue().toString().replace(""", "\""); + repos.add(new Repo(repoString, activityContext)); + } + } + } + + class BuildFromWeb extends AsyncTask { + + private TaskDelegate delegate; + + public BuildFromWeb(TaskDelegate delegate) { + this.delegate = delegate; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + + } + + @Override + protected void onProgressUpdate(String... values) { + super.onProgressUpdate(values); + prefs.edit().putBoolean("ignoreUpdateAlerts", false).apply(); + Toast.makeText(activityContext, "Refreshing online modules", Toast.LENGTH_SHORT).show(); + + } + + @Override + protected Void doInBackground(String... params) { + publishProgress(); + // Creating service handler class instance + WebRequest webreq = new WebRequest(); + + // Making a request to url and getting response + String token = activityContext.getString(R.string.some_string); + String url1 = activityContext.getString(R.string.url_main); + String jsonStr = webreq.makeWebServiceCall(url1 + Utils.procFile(token, activityContext), WebRequest.GET); + if (jsonStr != null && !jsonStr.isEmpty()) { + + try { + repos.clear(); + JSONArray jsonArray = new JSONArray(jsonStr); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonobject = jsonArray.getJSONObject(i); + String name = jsonobject.getString("name"); + String url = jsonobject.getString("url"); + String lastUpdate = jsonobject.getString("updated_at"); + String mId = ""; + String cacheUpdate = ""; + String manifestString = ""; + boolean doUpdate = true; + boolean hasCachedDate = false; + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + Map map = prefs.getAll(); + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue().toString().contains(url)) { + Log.d("Magisk", "RepoHelper: found matching URL"); + manifestString = entry.getValue().toString(); + String[] entryStrings = entry.getValue().toString().split("\n"); + for (String valueString : entryStrings) { + String[] valueSub = valueString.split("="); + if (valueSub[0].equals("id")) { + mId = valueSub[1]; + if (prefs.contains("updated_" + mId)) { + cacheUpdate = prefs.getString("updated_" + mId, ""); + hasCachedDate = true; + } + } + } + + } + } + try { + updatedDate = format.parse(lastUpdate); + Log.d("Magisk", "RepoHelper: Dates found, online is " + updatedDate + " and cached is " + cacheUpdate); + + if (hasCachedDate) { + + doUpdate = !cacheUpdate.equals(updatedDate.toString()); + Log.d("Magisk", "RepoHelper: DoUpdate is " + doUpdate); + } + + } catch (ParseException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + if (!name.contains("Repo.github.io")) { + if (doUpdate) { + repos.add(new Repo(name, url, updatedDate, activityContext)); + Log.d(TAG, "RepoHelper: Trying to add new repo for online refresh - " + name); + } else { + repos.add(new Repo(manifestString, activityContext)); + Log.d(TAG, "RepoHelper: Trying to add new repo using manifestString of " + mId); + } + } + for (int f = 0; f < repos.size(); f++) { + repos.get(f).fetch(); + } + } + } catch (JSONException e) { + e.printStackTrace(); + } + apiFail = false; + } else { + apiFail = true; + } + return null; + + } + + protected void onPostExecute(Void v) { + if (apiFail) { + Toast.makeText(activityContext, "GitHub API Limit reached, please try refreshing again in an hour.", Toast.LENGTH_LONG).show(); + } else { + Log.d("Magisk", "RepoHelper: postExecute fired"); + delegate.taskCompletionResult("Complete"); + BuildFromCache(); + + } + + } + } + + public interface TaskDelegate { + void taskCompletionResult(String result); + } + + public class CustomComparator implements Comparator { + @Override + public int compare(Repo o1, Repo o2) { + return o1.getName().compareTo(o2.getName()); + } + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/AnimationHelper.java b/app/src/main/java/com/topjohnwu/magisk/utils/AnimationHelper.java new file mode 100644 index 000000000..ebb1d3301 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/AnimationHelper.java @@ -0,0 +1,58 @@ +package com.topjohnwu.magisk.utils; + +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.Transformation; +import android.widget.LinearLayout; + +public class AnimationHelper { + public static void expand(final View v) { + v.measure(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + final int targetHeight = v.getMeasuredHeight(); + + v.getLayoutParams().height = 0; + v.setVisibility(View.VISIBLE); + Animation a = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + v.getLayoutParams().height = interpolatedTime == 1 + ? LinearLayout.LayoutParams.WRAP_CONTENT + : (int) (targetHeight * interpolatedTime); + v.requestLayout(); + } + + @Override + public boolean willChangeBounds() { + return true; + } + }; + + // 1dp/ms + a.setDuration((int) (targetHeight / v.getContext().getResources().getDisplayMetrics().density)); + v.startAnimation(a); + } + + public static void collapse(final View v) { + final int initialHeight = v.getMeasuredHeight(); + + Animation a = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + if (interpolatedTime == 1) { + v.setVisibility(View.GONE); + } else { + v.getLayoutParams().height = initialHeight - (int) (initialHeight * interpolatedTime); + v.requestLayout(); + } + } + + @Override + public boolean willChangeBounds() { + return true; + } + }; + // 1dp/ms + a.setDuration((int) (initialHeight / v.getContext().getResources().getDisplayMetrics().density)); + v.startAnimation(a); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/GitAgent.java b/app/src/main/java/com/topjohnwu/magisk/utils/GitAgent.java new file mode 100644 index 000000000..927c4cfb1 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/GitAgent.java @@ -0,0 +1,8 @@ +package com.topjohnwu.magisk.utils; + + +public class GitAgent { + + + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/Shell.java b/app/src/main/java/com/topjohnwu/magisk/utils/Shell.java index 718adb64f..18eaec072 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/Shell.java +++ b/app/src/main/java/com/topjohnwu/magisk/utils/Shell.java @@ -1,8 +1,5 @@ package com.topjohnwu.magisk.utils; -import android.os.AsyncTask; -import android.util.Log; - import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/StreamGobbler.java b/app/src/main/java/com/topjohnwu/magisk/utils/StreamGobbler.java index ce7c9eb5b..8b40775ab 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/StreamGobbler.java +++ b/app/src/main/java/com/topjohnwu/magisk/utils/StreamGobbler.java @@ -1,7 +1,5 @@ package com.topjohnwu.magisk.utils; -import android.util.Log; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; 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 c36710f21..84957bcc9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java +++ b/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java @@ -5,6 +5,7 @@ import android.app.Activity; import android.app.DownloadManager; import android.app.ProgressDialog; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -13,31 +14,54 @@ import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Environment; +import android.provider.DocumentsContract; import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; import android.support.v7.app.AlertDialog; +import android.util.Base64; +import android.util.Log; import android.view.View; import android.widget.Toast; import com.topjohnwu.magisk.ModulesFragment; import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.ReposFragment; import com.topjohnwu.magisk.module.Module; +import com.topjohnwu.magisk.module.Repo; +import com.topjohnwu.magisk.module.RepoHelper; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.URL; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; import java.util.List; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; + public class Utils { public static int magiskVersion, remoteMagiskVersion = -1, remoteAppVersion = -1; public static String magiskLink, magiskChangelog, appChangelog, appLink, phhLink, supersuLink; + private static final String TAG = "Magisk"; public static final String MAGISK_PATH = "/magisk"; public static final String MAGISK_CACHE_PATH = "/cache/magisk"; @@ -46,7 +70,8 @@ public class Utils { public static boolean fileExist(String path) { List ret; ret = Shell.sh("if [ -f " + path + " ]; then echo true; else echo false; fi"); - if (!Boolean.parseBoolean(ret.get(0)) && Shell.rootAccess()) ret = Shell.su("if [ -f " + path + " ]; then echo true; else echo false; fi"); + if (!Boolean.parseBoolean(ret.get(0)) && Shell.rootAccess()) + ret = Shell.su("if [ -f " + path + " ]; then echo true; else echo false; fi"); return Boolean.parseBoolean(ret.get(0)); } @@ -82,6 +107,10 @@ public class Utils { return ret; } + public Utils(Context context) { + Context appContext = context; + } + public static void downloadAndReceive(Context context, DownloadReceiver receiver, String link, String file) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { Toast.makeText(context, R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); @@ -105,15 +134,19 @@ public class Utils { long downloadID; public String mName; - public DownloadReceiver() {} - public DownloadReceiver(String name) { mName = name; } + public DownloadReceiver() { + } + + public DownloadReceiver(String name) { + mName = name; + } @Override public void onReceive(Context context, Intent intent) { mContext = context; DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); String action = intent.getAction(); - if(DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)){ + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { DownloadManager.Query query = new DownloadManager.Query(); query.setFilterById(downloadID); Cursor c = downloadManager.query(query); @@ -134,11 +167,14 @@ public class Utils { } } - public void setDownloadID(long id) { downloadID = id;} + public void setDownloadID(long id) { + downloadID = id; + } + public abstract void task(File file); } - public static class Initialize extends AsyncTask { + public static class Initialize extends AsyncTask { private Context mContext; @@ -156,19 +192,19 @@ public class Utils { } // Install Busybox and set as top priority - if (Shell.rootAccess()) { - String busybox = mContext.getApplicationInfo().nativeLibraryDir + "/libbusybox.so"; - Shell.su( - "rm -rf /data/busybox", - "mkdir -p /data/busybox", - "cp -af " + busybox + " /data/busybox/busybox", - "chmod 755 /data/busybox /data/busybox/busybox", - "chcon u:object_r:system_file:s0 /data/busybox /data/busybox/busybox", - "/data/busybox/busybox --install -s /data/busybox", - "rm -f /data/busybox/su", - "export PATH=/data/busybox:$PATH" - ); - } +// if (Shell.rootAccess()) { +// String busybox = mContext.getApplicationInfo().nativeLibraryDir + "/libbusybox.so"; +// Shell.su( +// "rm -rf /data/busybox", +// "mkdir -p /data/busybox", +// "cp -af " + busybox + " /data/busybox/busybox", +// "chmod 755 /data/busybox /data/busybox/busybox", +// "chcon u:object_r:system_file:s0 /data/busybox /data/busybox/busybox", +// "/data/busybox/busybox --install -s /data/busybox", +// "rm -f /data/busybox/su", +// "export PATH=/data/busybox:$PATH" +// ); +// } return null; } @@ -176,7 +212,7 @@ public class Utils { protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); if (!Shell.rootAccess()) { - Snackbar.make(((Activity)mContext).findViewById(android.R.id.content), R.string.no_root_access, Snackbar.LENGTH_LONG).show(); + Snackbar.make(((Activity) mContext).findViewById(android.R.id.content), R.string.no_root_access, Snackbar.LENGTH_LONG).show(); } } @@ -295,8 +331,7 @@ public class Utils { new AlertDialog.Builder(mContext) .setTitle(R.string.root_method_title) .setItems(new String[]{mContext.getString(R.string.phh), mContext.getString(R.string.supersu)}, (dialogInterface1, root) -> { - switch(root) - { + switch (root) { case 0: downloadAndReceive( mContext, @@ -358,12 +393,49 @@ public class Utils { } } + public static String procFile(String value, Context context) { + + String cryptoPass = context.getResources().getString(R.string.pass); + try { + DESKeySpec keySpec = new DESKeySpec(cryptoPass.getBytes("UTF8")); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey key = keyFactory.generateSecret(keySpec); + + byte[] encrypedPwdBytes = Base64.decode(value, Base64.DEFAULT); + // cipher is not thread safe + Cipher cipher = Cipher.getInstance("DES"); + cipher.init(Cipher.DECRYPT_MODE, key); + byte[] decrypedValueBytes = (cipher.doFinal(encrypedPwdBytes)); + + String decrypedValue = new String(decrypedValueBytes); + return decrypedValue; + + } catch (InvalidKeyException e) { + e.printStackTrace(); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (BadPaddingException e) { + e.printStackTrace(); + } catch (NoSuchPaddingException e) { + e.printStackTrace(); + } catch (IllegalBlockSizeException e) { + e.printStackTrace(); + } + return value; + } + public static class LoadModules extends AsyncTask { private Context mContext; + private boolean doReload; - public LoadModules(Context context) { + public LoadModules(Context context, boolean reload) { mContext = context; + doReload = reload; } @Override @@ -371,41 +443,154 @@ public class Utils { ModulesFragment.listModules.clear(); ModulesFragment.listModulesCache.clear(); List magisk = getModList(MAGISK_PATH); + Log.d("Magisk", "Utils: Reload called, loading modules from" + (doReload ? " the internet " : " cache")); List magiskCache = getModList(MAGISK_CACHE_PATH); + for (String mod : magisk) { - ModulesFragment.listModules.add(new Module(mod)); + Log.d("Magisk", "Utils: Adding module from string " + mod); + ModulesFragment.listModules.add(new Module(mod, mContext)); } + for (String mod : magiskCache) { - ModulesFragment.listModulesCache.add(new Module(mod)); + Log.d("Magisk", "Utils: Adding cache module from string " + mod); + ModulesFragment.listModulesCache.add(new Module(mod, mContext)); } + return null; } + + } + + public static class LoadRepos extends AsyncTask { + + private Context mContext; + private boolean doReload; + private RepoHelper.TaskDelegate mTaskDelegate; + + public LoadRepos(Context context, boolean reload, RepoHelper.TaskDelegate delegate) { + mContext = context; + doReload = reload; + mTaskDelegate = delegate; + } + + @Override + protected Void doInBackground(Void... voids) { + ReposFragment.mListRepos.clear(); + RepoHelper mr = new RepoHelper(); + List magiskRepos = mr.listRepos(mContext, doReload, mTaskDelegate); + + for (Repo repo : magiskRepos) { + Log.d("Magisk", "Utils: Adding repo from string " + repo.getId()); + ReposFragment.mListRepos.add(repo); + } + + return null; + } + } public static class FlashZIP extends AsyncTask { private String mPath, mName; + private Uri mUri; private ProgressDialog progress; + private File mFile; private Context mContext; + private List ret; + private boolean deleteFileAfter; public FlashZIP(Context context, String name, String path) { mContext = context; mName = name; mPath = path; + deleteFileAfter = false; + } + + public FlashZIP(Context context, Uri uRi) { + mContext = context; + mUri = uRi; + deleteFileAfter = true; + String file = ""; + final String docId = DocumentsContract.getDocumentId(mUri); + + Log.d("Magisk","Utils: FlashZip Running, " + docId + " and " + mUri.toString()); + String[] split = docId.split(":"); + mName = split[1]; + if (mName.contains("/")) { + split = mName.split("/"); + } + if (split[1].contains(".zip")) { + file = mContext.getFilesDir() + "/" + split[1]; + Log.d("Magisk", "Utils: FlashZip running for uRI " + mUri.toString()); + } else { + Log.e("Magisk", "Utils: error parsing Zipfile " + mUri.getPath()); + this.cancel(true); + } + ContentResolver contentResolver = mContext.getContentResolver(); + //contentResolver.takePersistableUriPermission(mUri, flags); + try { + InputStream in = contentResolver.openInputStream(mUri); + Log.d("Magisk", "Firing inputStream"); + mFile = createFileFromInputStream(in, file, mContext); + if (mFile != null) { + mPath = mFile.getPath(); + Log.d("Magisk", "Utils: Mpath is " + mPath); + } else { + Log.e("Magisk", "Utils: error creating file " + mUri.getPath()); + this.cancel(true); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + + // TODO handle non-primary volumes + + } + + private static File createFileFromInputStream(InputStream inputStream, String fileName, Context context) { + + try { + File f = new File(fileName); + f.setWritable(true, false); + OutputStream outputStream = new FileOutputStream(f); + byte buffer[] = new byte[1024]; + int length; + + while ((length = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, length); + } + + outputStream.close(); + inputStream.close(); + Log.d("Magisk", "Holy balls, I think it worked. File is " + f.getPath()); + return f; + + } catch (IOException e) { + System.out.println("error in creating a file"); + e.printStackTrace(); + } + + return null; } @Override protected void onPreExecute() { super.onPreExecute(); + progress = ProgressDialog.show(mContext, mContext.getString(R.string.zip_install_progress_title), mContext.getString(R.string.zip_install_progress_msg, mName)); } @Override protected Boolean doInBackground(Void... voids) { + if (mPath != null) { + Log.e("Magisk", "Utils: Error, flashZIP called without a valid zip file to flash."); + this.cancel(true); + return false; + } if (!Shell.rootAccess()) { return false; } else { - List ret = Shell.su( + ret = Shell.su( "rm -rf /data/tmp", "mkdir -p /data/tmp", "cp -af " + mPath + " /data/tmp/install.zip", @@ -413,7 +598,7 @@ public class Utils { "BOOTMODE=true sh /data/tmp/META-INF/com/google/android/update-binary dummy 1 /data/tmp/install.zip", "if [ $? -eq 0 ]; then echo true; else echo false; fi" ); - return Boolean.parseBoolean(ret.get(ret.size() -1)); + return ret != null && Boolean.parseBoolean(ret.get(ret.size() - 1)); } } @@ -421,6 +606,10 @@ public class Utils { protected void onPostExecute(Boolean result) { super.onPostExecute(result); Shell.su("rm -rf /data/tmp"); + if (deleteFileAfter) { + Shell.su("rm -rf " + mPath); + Log.d("Magisk", "Utils: Deleting file " + mPath); + } progress.dismiss(); if (!result) { Toast.makeText(mContext, mContext.getString(R.string.manual_install, mPath), Toast.LENGTH_LONG).show(); @@ -444,4 +633,5 @@ public class Utils { void onItemClick(View view, int position); } + } diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/WebRequest.java b/app/src/main/java/com/topjohnwu/magisk/utils/WebRequest.java new file mode 100644 index 000000000..d01649e97 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/WebRequest.java @@ -0,0 +1,123 @@ +package com.topjohnwu.magisk.utils; + +import android.util.Log; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; + +import javax.net.ssl.HttpsURLConnection; + +public class WebRequest { + + static String response = null; + public final static int GET = 1; + public final static int POST = 2; + private boolean addNewLine; + + //Constructor with no parameter + public WebRequest() { + + } + + /** + * Making web service call + * + * @url - url to make request + * @requestmethod - http request method + */ + public String makeWebServiceCall(String url, int requestmethod) { + addNewLine=false; + Log.d("Magisk","WebRequest: Service call received for URL " + url); + return this.makeWebServiceCall(url, requestmethod, null); + + + } + + public String makeWebServiceCall(String url, int requestmethod, boolean addNewLines) { + addNewLine = addNewLines; + Log.d("Magisk","WebRequest: Service call(bool) received for URL " + url); + return this.makeWebServiceCall(url, requestmethod, null); + + } + + /** + * Making service call + * + * @url - url to make request + * @requestmethod - http request method + * @params - http request params + */ + public String makeWebServiceCall(String urladdress, int requestmethod, + HashMap params) { + URL url; + String response = ""; + try { + url = new URL(urladdress); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setReadTimeout(15000); + conn.setConnectTimeout(15000); + conn.setDoInput(true); + + if (requestmethod == POST) { + conn.setRequestMethod("POST"); + } else if (requestmethod == GET) { + conn.setRequestMethod("GET"); + } + + if (params != null) { + OutputStream os = conn.getOutputStream(); + BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(os, "UTF-8")); + + StringBuilder result = new StringBuilder(); + boolean first = true; + for (Map.Entry entry : params.entrySet()) { + if (first) + first = false; + else + result.append("&"); + + result.append(URLEncoder.encode(entry.getKey(), "UTF-8")); + result.append("="); + result.append(URLEncoder.encode(entry.getValue(), "UTF-8")); + } + + writer.write(result.toString()); + + writer.flush(); + writer.close(); + os.close(); + } + + int responseCode = conn.getResponseCode(); + + if (responseCode == HttpsURLConnection.HTTP_OK) { + String line; + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + while ((line = br.readLine()) != null) { + if (addNewLine) { + response += line + "\n"; + } else { + response += line; + } + } + } else { + response = ""; + } + } catch (Exception e) { + e.printStackTrace(); + } + + return response; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/WebWindow.java b/app/src/main/java/com/topjohnwu/magisk/utils/WebWindow.java new file mode 100644 index 000000000..d48136c7f --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/WebWindow.java @@ -0,0 +1,33 @@ +package com.topjohnwu.magisk.utils; + +import android.content.Context; +import android.support.v7.app.AlertDialog; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; + + +public class WebWindow { + +public WebWindow(String title, String url, Context context) { + AlertDialog.Builder alert = new AlertDialog.Builder(context); + alert.setTitle(title); + + WebView wv = new WebView(context); + wv.loadUrl(url); + wv.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + view.loadUrl(url); + return true; + } + }); + + alert.setView(wv); + alert.setNegativeButton("Close", (dialog, id) -> { + dialog.dismiss(); + }); + alert.show(); +} + +} diff --git a/app/src/main/res/drawable/ic_author.xml b/app/src/main/res/drawable/ic_author.xml new file mode 100644 index 000000000..81cffc428 --- /dev/null +++ b/app/src/main/res/drawable/ic_author.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_changelog.xml b/app/src/main/res/drawable/ic_changelog.xml new file mode 100644 index 000000000..f4b1dd42b --- /dev/null +++ b/app/src/main/res/drawable/ic_changelog.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_done_black.xml b/app/src/main/res/drawable/ic_done_black.xml new file mode 100644 index 000000000..7affe9ba9 --- /dev/null +++ b/app/src/main/res/drawable/ic_done_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_download_black.xml b/app/src/main/res/drawable/ic_file_download_black.xml new file mode 100644 index 000000000..d05655222 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_download_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_support.xml b/app/src/main/res/drawable/ic_support.xml new file mode 100644 index 000000000..6ab452192 --- /dev/null +++ b/app/src/main/res/drawable/ic_support.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_system_update_alt_black.xml b/app/src/main/res/drawable/ic_system_update_alt_black.xml new file mode 100644 index 000000000..fb2d85e57 --- /dev/null +++ b/app/src/main/res/drawable/ic_system_update_alt_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/list_item_module.xml b/app/src/main/res/layout/list_item_module.xml index 6a22df864..d8eca5c10 100644 --- a/app/src/main/res/layout/list_item_module.xml +++ b/app/src/main/res/layout/list_item_module.xml @@ -1,92 +1,183 @@ + xmlns:card_view="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginBottom="3dip" + android:layout_marginLeft="8dip" + android:layout_marginRight="8dip" + android:layout_marginTop="3dip" + android:background="?android:attr/selectableItemBackground" + android:minHeight="?android:attr/listPreferredItemHeight" + card_view:cardCornerRadius="2dp" + card_view:cardElevation="2dp"> - - - + android:padding="10dp" + android:layout_gravity="center_vertical"> + android:textIsSelectable="false" /> - + + + + + + + + + + + + + + + + + + + + + + android:textIsSelectable="false" /> + android:visibility="gone" /> - + + + + + + + + + + + + + + + + + + + + - - - \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_repo.xml b/app/src/main/res/layout/list_item_repo.xml new file mode 100644 index 000000000..b8bcb8f61 --- /dev/null +++ b/app/src/main/res/layout/list_item_repo.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/single_module_fragment.xml b/app/src/main/res/layout/single_module_fragment.xml index 0b9aa8c9b..336fbdb93 100644 --- a/app/src/main/res/layout/single_module_fragment.xml +++ b/app/src/main/res/layout/single_module_fragment.xml @@ -1,14 +1,19 @@ - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/single_repo_fragment.xml b/app/src/main/res/layout/single_repo_fragment.xml new file mode 100644 index 000000000..173657d94 --- /dev/null +++ b/app/src/main/res/layout/single_repo_fragment.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/drawer.xml b/app/src/main/res/menu/drawer.xml index da2e5112a..1276fb206 100644 --- a/app/src/main/res/menu/drawer.xml +++ b/app/src/main/res/menu/drawer.xml @@ -25,6 +25,11 @@ android:icon="@drawable/ic_extension" android:title="@string/modules"/> + + #FFC107 #212121 #FFFFFF - + #757575 #F44336 #4CAF50 #2196F3 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 8eb8263ae..065a40f8a 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -2,4 +2,5 @@ 650dp 500dp + 100dp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9140d87ca..1132132b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,10 +43,15 @@ Cache modules No modules found + An update is available! + Module is up-to-date + Module is installed + Module is not installed Module will be removed at next reboot Module will not be removed at next reboot Module will be disabled at next reboot Module will be enabled at next reboot + Created by Save to SD @@ -90,4 +95,8 @@ phh\'s superuser SuperSU It seems that you have incompatible root installed\nDo you want to install Magisk compatible root now? + MagiskRox666 + GTYybRBTYf5his9kQ16ZNO7qgkBJ/5MyVe4CGceAOIoXgSnnk8FTd4F1dE9p5Eus + Downloads + https://api.github.com/orgs/Magisk-Modules-Repo/repos?access_token=