* A set of tools for UI.
*/
public class VUiKit {
private static final AndroidDeferredManager gDM = new AndroidDeferredManager();
private static final Handler gUiHandler = new Handler(Looper.getMainLooper());
public static AndroidDeferredManager defer() {
return gDM;
}
public static int dpToPx(Context context, int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
context.getResources().getDisplayMetrics());
}
public static void post(Runnable r) {
gUiHandler.post(r);
}
public static void postDelayed(long delay, Runnable r) {
gUiHandler.postDelayed(r, delay);
}
public static void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
================================================
FILE: app/src/main/java/io/virtualapp/delegate/MyAppRequestListener.java
================================================
package io.virtualapp.delegate;
import android.content.Context;
import android.widget.Toast;
import com.lody.virtual.client.core.InstallStrategy;
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.remote.InstallResult;
import java.io.IOException;
/**
* @author Lody
*/
public class MyAppRequestListener implements VirtualCore.AppRequestListener {
private final Context context;
public MyAppRequestListener(Context context) {
this.context = context;
}
@Override
public void onRequestInstall(String path) {
Toast.makeText(context, "Installing: " + path, Toast.LENGTH_SHORT).show();
InstallResult res = VirtualCore.get().installPackage(path, InstallStrategy.UPDATE_IF_EXIST);
if (res.isSuccess) {
try {
VirtualCore.get().preOpt(res.packageName);
} catch (IOException e) {
e.printStackTrace();
}
if (res.isUpdate) {
Toast.makeText(context, "Update: " + res.packageName + " success!", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "Install: " + res.packageName + " success!", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(context, "Install failed: " + res.error, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onRequestUninstall(String pkg) {
Toast.makeText(context, "Uninstall: " + pkg, Toast.LENGTH_SHORT).show();
}
}
================================================
FILE: app/src/main/java/io/virtualapp/delegate/MyComponentDelegate.java
================================================
package io.virtualapp.delegate;
import android.app.Activity;
import android.content.Intent;
import com.lody.virtual.client.hook.delegate.ComponentDelegate;
import com.lody.virtual.helper.utils.Reflect;
import java.io.File;
public class MyComponentDelegate implements ComponentDelegate {
@Override
public void beforeActivityCreate(Activity activity) {
}
@Override
public void beforeActivityResume(Activity activity) {
}
@Override
public void beforeActivityPause(Activity activity) {
}
@Override
public void beforeActivityDestroy(Activity activity) {
}
@Override
public void afterActivityCreate(Activity activity) {
}
@Override
public void afterActivityResume(Activity activity) {
}
@Override
public void afterActivityPause(Activity activity) {
}
@Override
public void afterActivityDestroy(Activity activity) {
}
@Override
public void onSendBroadcast(Intent intent) {
}
}
================================================
FILE: app/src/main/java/io/virtualapp/delegate/MyPhoneInfoDelegate.java
================================================
package io.virtualapp.delegate;
import com.lody.virtual.client.hook.delegate.PhoneInfoDelegate;
/**
* Fake the Device ID.
*/
public class MyPhoneInfoDelegate implements PhoneInfoDelegate {
@Override
public String getDeviceId(String oldDeviceId, int userId) {
return oldDeviceId;
}
@Override
public String getBluetoothAddress(String oldAddress, int userId) {
return oldAddress;
}
@Override
public String getMacAddress(String oldAddress, int userId) {
return oldAddress;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/delegate/MyTaskDescriptionDelegate.java
================================================
package io.virtualapp.delegate;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.Application;
import android.os.Build;
import com.lody.virtual.client.hook.delegate.TaskDescriptionDelegate;
import com.lody.virtual.os.VUserManager;
/**
* Patch the task description with the (Virtual) user name
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class MyTaskDescriptionDelegate implements TaskDescriptionDelegate {
@Override
public ActivityManager.TaskDescription getTaskDescription(ActivityManager.TaskDescription oldTaskDescription) {
String labelPrefix = "[" + VUserManager.get().getUserName() + "] ";
if (!oldTaskDescription.getLabel().startsWith(labelPrefix)) {
// Is it really necessary?
return new ActivityManager.TaskDescription(labelPrefix + oldTaskDescription.getLabel(), oldTaskDescription.getIcon(), oldTaskDescription.getPrimaryColor());
} else {
return oldTaskDescription;
}
}
}
================================================
FILE: app/src/main/java/io/virtualapp/effects/ExplosionAnimator.java
================================================
package io.virtualapp.effects;
import java.util.Random;
import android.animation.ValueAnimator;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;
import io.virtualapp.VApp;
import io.virtualapp.abs.ui.VUiKit;
public class ExplosionAnimator extends ValueAnimator {
private static final Interpolator DEFAULT_INTERPOLATOR = new AccelerateInterpolator(0.6f);
private static final float END_VALUE = 1.4f;
private static final float X = VUiKit.dpToPx(VApp.getApp(), 5);
private static final float Y = VUiKit.dpToPx(VApp.getApp(), 20);
private static final float V = VUiKit.dpToPx(VApp.getApp(), 2);
private static final float W = VUiKit.dpToPx(VApp.getApp(), 1);
static long DEFAULT_DURATION = 0x450;
private Paint mPaint;
private Particle[] mParticles;
private Rect mBound;
private View mContainer;
public ExplosionAnimator(View container, Bitmap bitmap, Rect bound) {
mPaint = new Paint();
mBound = new Rect(bound);
int partLen = 15;
mParticles = new Particle[partLen * partLen];
Random random = new Random(System.currentTimeMillis());
int w = bitmap.getWidth() / (partLen + 2);
int h = bitmap.getHeight() / (partLen + 2);
for (int i = 0; i < partLen; i++) {
for (int j = 0; j < partLen; j++) {
mParticles[(i * partLen) + j] = generateParticle(bitmap.getPixel((j + 1) * w, (i + 1) * h), random);
}
}
mContainer = container;
setFloatValues(0f, END_VALUE);
setInterpolator(DEFAULT_INTERPOLATOR);
setDuration(DEFAULT_DURATION);
}
private Particle generateParticle(int color, Random random) {
Particle particle = new Particle();
particle.color = color;
particle.radius = V;
if (random.nextFloat() < 0.2f) {
particle.baseRadius = V + ((X - V) * random.nextFloat());
} else {
particle.baseRadius = W + ((V - W) * random.nextFloat());
}
float nextFloat = random.nextFloat();
particle.top = mBound.height() * ((0.18f * random.nextFloat()) + 0.2f);
particle.top = nextFloat < 0.2f ? particle.top : particle.top + ((particle.top * 0.2f) * random.nextFloat());
particle.bottom = (mBound.height() * (random.nextFloat() - 0.5f)) * 1.8f;
float f = nextFloat < 0.2f
? particle.bottom
: nextFloat < 0.8f ? particle.bottom * 0.6f : particle.bottom * 0.3f;
particle.bottom = f;
particle.mag = 4.0f * particle.top / particle.bottom;
particle.neg = (-particle.mag) / particle.bottom;
f = mBound.centerX() + (Y * (random.nextFloat() - 0.5f));
particle.baseCx = f;
particle.cx = f;
f = mBound.centerY() + (Y * (random.nextFloat() - 0.5f));
particle.baseCy = f;
particle.cy = f;
particle.life = END_VALUE / 10 * random.nextFloat();
particle.overflow = 0.4f * random.nextFloat();
particle.alpha = 1f;
return particle;
}
public boolean draw(Canvas canvas) {
if (!isStarted()) {
return false;
}
for (Particle particle : mParticles) {
particle.advance((float) getAnimatedValue());
if (particle.alpha > 0f) {
mPaint.setColor(particle.color);
mPaint.setAlpha((int) (Color.alpha(particle.color) * particle.alpha));
canvas.drawCircle(particle.cx, particle.cy, particle.radius, mPaint);
}
}
mContainer.invalidate();
return true;
}
@Override
public void start() {
super.start();
mContainer.invalidate(mBound);
}
private class Particle {
float alpha;
int color;
float cx;
float cy;
float radius;
float baseCx;
float baseCy;
float baseRadius;
float top;
float bottom;
float mag;
float neg;
float life;
float overflow;
public void advance(float factor) {
float f = 0f;
float normalization = factor / END_VALUE;
if (normalization < life || normalization > 1f - overflow) {
alpha = 0f;
return;
}
normalization = (normalization - life) / (1f - life - overflow);
float f2 = normalization * END_VALUE;
if (normalization >= 0.7f) {
f = (normalization - 0.7f) / 0.3f;
}
alpha = 1f - f;
f = bottom * f2;
cx = baseCx + f;
cy = (float) (baseCy - this.neg * Math.pow(f, 2.0)) - f * mag;
radius = V + (baseRadius - V) * f2;
}
}
}
================================================
FILE: app/src/main/java/io/virtualapp/effects/ExplosionField.java
================================================
package io.virtualapp.effects;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.ImageView;
import io.virtualapp.VApp;
import io.virtualapp.abs.ui.VUiKit;
public class ExplosionField extends View {
private static final Canvas sCanvas = new Canvas();
private List mExplosions = new ArrayList<>();
private int[] mExpandInset = new int[2];
public ExplosionField(Context context) {
super(context);
init();
}
public ExplosionField(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ExplosionField(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public static Bitmap createBitmapFromView(View view) {
if (view instanceof ImageView) {
Drawable drawable = ((ImageView) view).getDrawable();
if (drawable != null && drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
}
view.clearFocus();
Bitmap bitmap = createBitmapSafely(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888, 1);
if (bitmap != null) {
synchronized (sCanvas) {
Canvas canvas = sCanvas;
canvas.setBitmap(bitmap);
view.draw(canvas);
canvas.setBitmap(null);
}
}
return bitmap;
}
public static Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int retryCount) {
try {
return Bitmap.createBitmap(width, height, config);
} catch (OutOfMemoryError e) {
e.printStackTrace();
if (retryCount > 0) {
System.gc();
return createBitmapSafely(width, height, config, retryCount - 1);
}
return null;
}
}
public static ExplosionField attachToWindow(Activity activity) {
ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
ExplosionField explosionField = new ExplosionField(activity);
rootView.addView(explosionField,
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return explosionField;
}
public static ExplosionField attachToWindow(ViewGroup rootView, Activity activity) {
ExplosionField explosionField = new ExplosionField(activity);
rootView.addView(explosionField,
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return explosionField;
}
private void init() {
Arrays.fill(mExpandInset, VUiKit.dpToPx(VApp.getApp(), 32));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (ExplosionAnimator explosion : mExplosions) {
explosion.draw(canvas);
}
}
public void expandExplosionBound(int dx, int dy) {
mExpandInset[0] = dx;
mExpandInset[1] = dy;
}
public void explode(Bitmap bitmap, Rect bound, long startDelay, long duration) {
final ExplosionAnimator explosion = new ExplosionAnimator(this, bitmap, bound);
explosion.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mExplosions.remove(animation);
}
});
explosion.setStartDelay(startDelay);
explosion.setDuration(duration);
mExplosions.add(explosion);
explosion.start();
}
public void explode(final View view) {
explode(view, null);
}
public void explode(final View view, OnExplodeFinishListener listener) {
Rect r = new Rect();
view.getGlobalVisibleRect(r);
int[] location = new int[2];
getLocationOnScreen(location);
r.offset(-location[0], -location[1]);
r.inset(-mExpandInset[0], -mExpandInset[1]);
int startDelay = 100;
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(150);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
Random random = new Random();
@Override
public void onAnimationUpdate(ValueAnimator animation) {
view.setTranslationX((random.nextFloat() - 0.5f) * view.getWidth() * 0.05f);
view.setTranslationY((random.nextFloat() - 0.5f) * view.getHeight() * 0.05f);
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (listener != null) {
listener.onExplodeFinish(view);
}
}
});
animator.start();
view.animate().setDuration(150).setStartDelay(startDelay).scaleX(0f).scaleY(0f).alpha(0f).start();
explode(createBitmapFromView(view), r, startDelay, ExplosionAnimator.DEFAULT_DURATION);
}
public void clear() {
mExplosions.clear();
invalidate();
}
public interface OnExplodeFinishListener {
void onExplodeFinish(View v);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/FlurryROMCollector.java
================================================
package io.virtualapp.home;
import android.hardware.Camera;
import android.os.Build;
import android.util.Log;
import com.flurry.android.FlurryAgent;
import com.flurry.android.FlurryEventRecordStatus;
import com.lody.virtual.client.natives.NativeMethods;
import com.lody.virtual.helper.utils.Reflect;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* @author Lody
*/
public class FlurryROMCollector {
private static final String TAG = FlurryROMCollector.class.getSimpleName();
public static void startCollect() {
Log.d(TAG, "start collect...");
NativeMethods.init();
if (NativeMethods.gCameraNativeSetup == null) {
reportCameraNativeSetup();
}
Log.d(TAG, "end collect...");
}
private static void reportCameraNativeSetup() {
for (Method method : Camera.class.getDeclaredMethods()) {
if ("native_setup".equals(method.getName())) {
FlurryEventRecordStatus status =
FlurryAgent.logEvent("camera::native_setup", createLogContent("method_details", Reflect.getMethodDetails(method)));
Log.d(TAG, "report CNS: " + status);
break;
}
}
}
private static Map createLogContent(String tag, String value) {
Map content = new HashMap<>(3);
addRomInfo(content);
content.put(tag, value);
return content;
}
private static void addRomInfo(Map content) {
content.put("device", Build.DEVICE);
content.put("brand", Build.BRAND);
content.put("manufacturer", Build.MANUFACTURER);
content.put("display", Build.DISPLAY);
content.put("model", Build.MODEL);
content.put("protect", Build.PRODUCT);
content.put("sdk_version", "API-" + Build.VERSION.SDK_INT);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/HomeActivity.java
================================================
package io.virtualapp.home;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.OrientationHelper;
import android.support.v7.widget.PopupMenu;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.ContextThemeWrapper;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import com.lody.virtual.GmsSupport;
import com.lody.virtual.client.stub.ChooseTypeAndAccountActivity;
import com.lody.virtual.os.VUserInfo;
import com.lody.virtual.os.VUserManager;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import io.virtualapp.R;
import io.virtualapp.VCommends;
import io.virtualapp.abs.nestedadapter.SmartRecyclerAdapter;
import io.virtualapp.abs.ui.VActivity;
import io.virtualapp.abs.ui.VUiKit;
import io.virtualapp.home.adapters.LaunchpadAdapter;
import io.virtualapp.home.adapters.decorations.ItemOffsetDecoration;
import io.virtualapp.home.models.AddAppButton;
import io.virtualapp.home.models.AppData;
import io.virtualapp.home.models.AppInfoLite;
import io.virtualapp.home.models.EmptyAppData;
import io.virtualapp.home.models.PackageAppData;
import io.virtualapp.widgets.TwoGearsView;
import static android.support.v7.widget.helper.ItemTouchHelper.ACTION_STATE_DRAG;
import static android.support.v7.widget.helper.ItemTouchHelper.DOWN;
import static android.support.v7.widget.helper.ItemTouchHelper.END;
import static android.support.v7.widget.helper.ItemTouchHelper.LEFT;
import static android.support.v7.widget.helper.ItemTouchHelper.RIGHT;
import static android.support.v7.widget.helper.ItemTouchHelper.START;
import static android.support.v7.widget.helper.ItemTouchHelper.UP;
/**
* @author Lody
*/
public class HomeActivity extends VActivity implements HomeContract.HomeView {
private static final String TAG = HomeActivity.class.getSimpleName();
private HomeContract.HomePresenter mPresenter;
private TwoGearsView mLoadingView;
private RecyclerView mLauncherView;
private View mMenuView;
private PopupMenu mPopupMenu;
private View mBottomArea;
private View mCreateShortcutBox;
private TextView mCreateShortcutTextView;
private View mDeleteAppBox;
private TextView mDeleteAppTextView;
private LaunchpadAdapter mLaunchpadAdapter;
private Handler mUiHandler;
public static void goHome(Context context) {
Intent intent = new Intent(context, HomeActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
overridePendingTransition(0, 0);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
mUiHandler = new Handler(Looper.getMainLooper());
bindViews();
initLaunchpad();
initMenu();
new HomePresenterImpl(this).start();
}
private void initMenu() {
mPopupMenu = new PopupMenu(new ContextThemeWrapper(this, R.style.Theme_AppCompat_Light), mMenuView);
Menu menu = mPopupMenu.getMenu();
setIconEnable(menu, true);
menu.add("Accounts").setIcon(R.drawable.ic_account).setOnMenuItemClickListener(item -> {
List users = VUserManager.get().getUsers();
List names = new ArrayList<>(users.size());
for (VUserInfo info : users) {
names.add(info.name);
}
CharSequence[] items = new CharSequence[names.size()];
for (int i = 0; i < names.size(); i++) {
items[i] = names.get(i);
}
new AlertDialog.Builder(this)
.setTitle("Please select an user")
.setItems(items, (dialog, which) -> {
VUserInfo info = users.get(which);
Intent intent = new Intent(this, ChooseTypeAndAccountActivity.class);
intent.putExtra(ChooseTypeAndAccountActivity.KEY_USER_ID, info.id);
startActivity(intent);
}).show();
return false;
});
menu.add("Virtual Storage").setIcon(R.drawable.ic_vs).setOnMenuItemClickListener(item -> {
Toast.makeText(this, "The coming", Toast.LENGTH_SHORT).show();
return false;
});
menu.add("Notification").setIcon(R.drawable.ic_notification).setOnMenuItemClickListener(item -> {
Toast.makeText(this, "The coming", Toast.LENGTH_SHORT).show();
return false;
});
menu.add("Settings").setIcon(R.drawable.ic_settings).setOnMenuItemClickListener(item -> {
Toast.makeText(this, "The coming", Toast.LENGTH_SHORT).show();
return false;
});
mMenuView.setOnClickListener(v -> mPopupMenu.show());
}
private static void setIconEnable(Menu menu, boolean enable) {
try {
@SuppressLint("PrivateApi")
Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class);
m.setAccessible(true);
m.invoke(menu, enable);
} catch (Exception e) {
e.printStackTrace();
}
}
private void bindViews() {
mLoadingView = (TwoGearsView) findViewById(R.id.pb_loading_app);
mLauncherView = (RecyclerView) findViewById(R.id.home_launcher);
mMenuView = findViewById(R.id.home_menu);
mBottomArea = findViewById(R.id.bottom_area);
mCreateShortcutBox = findViewById(R.id.create_shortcut_area);
mCreateShortcutTextView = (TextView) findViewById(R.id.create_shortcut_text);
mDeleteAppBox = findViewById(R.id.delete_app_area);
mDeleteAppTextView = (TextView) findViewById(R.id.delete_app_text);
}
private void initLaunchpad() {
mLauncherView.setHasFixedSize(true);
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, OrientationHelper.VERTICAL);
mLauncherView.setLayoutManager(layoutManager);
mLaunchpadAdapter = new LaunchpadAdapter(this);
SmartRecyclerAdapter wrap = new SmartRecyclerAdapter(mLaunchpadAdapter);
View footer = new View(this);
footer.setLayoutParams(new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, VUiKit.dpToPx(this, 60)));
wrap.setFooterView(footer);
mLauncherView.setAdapter(wrap);
mLauncherView.addItemDecoration(new ItemOffsetDecoration(this, R.dimen.desktop_divider));
ItemTouchHelper touchHelper = new ItemTouchHelper(new LauncherTouchCallback());
touchHelper.attachToRecyclerView(mLauncherView);
mLaunchpadAdapter.setAppClickListener((pos, data) -> {
if (!data.isLoading()) {
if (data instanceof AddAppButton) {
onAddAppButtonClick();
}
mLaunchpadAdapter.notifyItemChanged(pos);
mPresenter.launchApp(data);
}
});
}
private void onAddAppButtonClick() {
ListAppActivity.gotoListApp(this);
}
private void deleteApp(int position) {
AppData data = mLaunchpadAdapter.getList().get(position);
new AlertDialog.Builder(this)
.setTitle("Delete app")
.setMessage("Do you want to delete " + data.getName() + "?")
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
mPresenter.deleteApp(data);
})
.setNegativeButton(android.R.string.no, null)
.show();
}
private void createShortcut(int position) {
AppData model = mLaunchpadAdapter.getList().get(position);
if (model instanceof PackageAppData) {
mPresenter.createShortcut(model);
}
}
@Override
public void setPresenter(HomeContract.HomePresenter presenter) {
mPresenter = presenter;
}
@Override
public void showBottomAction() {
mBottomArea.setTranslationY(mBottomArea.getHeight());
mBottomArea.setVisibility(View.VISIBLE);
mBottomArea.animate().translationY(0).setDuration(500L).start();
}
@Override
public void hideBottomAction() {
mBottomArea.setTranslationY(0);
ObjectAnimator transAnim = ObjectAnimator.ofFloat(mBottomArea, "translationY", 0, mBottomArea.getHeight());
transAnim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
mBottomArea.setVisibility(View.GONE);
}
@Override
public void onAnimationCancel(Animator animator) {
mBottomArea.setVisibility(View.GONE);
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
transAnim.setDuration(500L);
transAnim.start();
}
@Override
public void showLoading() {
mLoadingView.setVisibility(View.VISIBLE);
mLoadingView.startAnim();
}
@Override
public void hideLoading() {
mLoadingView.setVisibility(View.GONE);
mLoadingView.stopAnim();
}
@Override
public void loadFinish(List list) {
list.add(new AddAppButton(this));
mLaunchpadAdapter.setList(list);
hideLoading();
}
@Override
public void loadError(Throwable err) {
err.printStackTrace();
hideLoading();
}
@Override
public void showGuide() {
}
@Override
public void addAppToLauncher(AppData model) {
List dataList = mLaunchpadAdapter.getList();
boolean replaced = false;
for (int i = 0; i < dataList.size(); i++) {
AppData data = dataList.get(i);
if (data instanceof EmptyAppData) {
mLaunchpadAdapter.replace(i, model);
replaced = true;
break;
}
}
if (!replaced) {
mLaunchpadAdapter.add(model);
mLauncherView.smoothScrollToPosition(mLaunchpadAdapter.getItemCount() - 1);
}
}
@Override
public void removeAppToLauncher(AppData model) {
mLaunchpadAdapter.remove(model);
}
@Override
public void refreshLauncherItem(AppData model) {
mLaunchpadAdapter.refresh(model);
}
@Override
public void askInstallGms() {
new AlertDialog.Builder(this)
.setTitle("Hi")
.setMessage("We found that your device has been installed the Google service, whether you need to install them?")
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
defer().when(() -> {
GmsSupport.installGApps(0);
}).done((res) -> {
mPresenter.dataChanged();
});
})
.setNegativeButton(android.R.string.cancel, (dialog, which) ->
Toast.makeText(HomeActivity.this, "You can also find it in the Settings~", Toast.LENGTH_LONG).show())
.setCancelable(false)
.show();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && data != null) {
List appList = data.getParcelableArrayListExtra(VCommends.EXTRA_APP_INFO_LIST);
if (appList != null) {
for (AppInfoLite info : appList) {
mPresenter.addApp(info);
}
}
}
}
private class LauncherTouchCallback extends ItemTouchHelper.SimpleCallback {
int[] location = new int[2];
boolean upAtDeleteAppArea;
boolean upAtCreateShortcutArea;
RecyclerView.ViewHolder dragHolder;
LauncherTouchCallback() {
super(UP | DOWN | LEFT | RIGHT | START | END, 0);
}
@Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, int viewSizeOutOfBounds, int totalSize, long msSinceStartScroll) {
return 0;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
try {
AppData data = mLaunchpadAdapter.getList().get(viewHolder.getAdapterPosition());
if (!data.canReorder()) {
return makeMovementFlags(0, 0);
}
} catch (IndexOutOfBoundsException e) {
e.printStackTrace();
}
return super.getMovementFlags(recyclerView, viewHolder);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int pos = viewHolder.getAdapterPosition();
int targetPos = target.getAdapterPosition();
mLaunchpadAdapter.moveItem(pos, targetPos);
return true;
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
if (viewHolder instanceof LaunchpadAdapter.ViewHolder) {
if (actionState == ACTION_STATE_DRAG) {
if (dragHolder != viewHolder) {
dragHolder = viewHolder;
viewHolder.itemView.setScaleX(1.2f);
viewHolder.itemView.setScaleY(1.2f);
if (mBottomArea.getVisibility() == View.GONE) {
showBottomAction();
}
}
}
}
super.onSelectedChanged(viewHolder, actionState);
}
@Override
public boolean canDropOver(RecyclerView recyclerView, RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) {
if (upAtCreateShortcutArea || upAtDeleteAppArea) {
return false;
}
try {
AppData data = mLaunchpadAdapter.getList().get(target.getAdapterPosition());
return data.canReorder();
} catch (IndexOutOfBoundsException e) {
e.printStackTrace();
}
return false;
}
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (viewHolder instanceof LaunchpadAdapter.ViewHolder) {
LaunchpadAdapter.ViewHolder holder = (LaunchpadAdapter.ViewHolder) viewHolder;
viewHolder.itemView.setScaleX(1f);
viewHolder.itemView.setScaleY(1f);
viewHolder.itemView.setBackgroundColor(holder.color);
}
super.clearView(recyclerView, viewHolder);
if (dragHolder == viewHolder) {
if (mBottomArea.getVisibility() == View.VISIBLE) {
mUiHandler.postDelayed(HomeActivity.this::hideBottomAction, 200L);
if (upAtCreateShortcutArea) {
createShortcut(viewHolder.getAdapterPosition());
} else if (upAtDeleteAppArea) {
deleteApp(viewHolder.getAdapterPosition());
}
}
dragHolder = null;
}
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
}
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
if (actionState != ACTION_STATE_DRAG || !isCurrentlyActive) {
return;
}
View itemView = viewHolder.itemView;
itemView.getLocationInWindow(location);
int x = (int) (location[0] + dX);
int y = (int) (location[1] + dY);
mBottomArea.getLocationInWindow(location);
int baseLine = location[1] - mBottomArea.getHeight();
if (y >= baseLine) {
mDeleteAppBox.getLocationInWindow(location);
int deleteAppAreaStartX = location[0];
if (x < deleteAppAreaStartX) {
upAtCreateShortcutArea = true;
upAtDeleteAppArea = false;
mCreateShortcutTextView.setTextColor(Color.parseColor("#0099cc"));
mDeleteAppTextView.setTextColor(Color.WHITE);
} else {
upAtDeleteAppArea = true;
upAtCreateShortcutArea = false;
mDeleteAppTextView.setTextColor(Color.parseColor("#0099cc"));
mCreateShortcutTextView.setTextColor(Color.WHITE);
}
} else {
upAtCreateShortcutArea = false;
upAtDeleteAppArea = false;
mDeleteAppTextView.setTextColor(Color.WHITE);
mCreateShortcutTextView.setTextColor(Color.WHITE);
}
}
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/HomeContract.java
================================================
package io.virtualapp.home;
import java.util.List;
import io.virtualapp.abs.BasePresenter;
import io.virtualapp.abs.BaseView;
import io.virtualapp.home.models.AppData;
import io.virtualapp.home.models.AppInfoLite;
/**
* @author Lody
*/
/* package */ class HomeContract {
/* package */ interface HomeView extends BaseView {
void showBottomAction();
void hideBottomAction();
void showLoading();
void hideLoading();
void loadFinish(List appModels);
void loadError(Throwable err);
void showGuide();
void addAppToLauncher(AppData model);
void removeAppToLauncher(AppData model);
void refreshLauncherItem(AppData model);
void askInstallGms();
}
/* package */ interface HomePresenter extends BasePresenter {
void launchApp(AppData data);
void dataChanged();
void addApp(AppInfoLite info);
void deleteApp(AppData data);
void createShortcut(AppData data);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/HomePresenterImpl.java
================================================
package io.virtualapp.home;
import android.app.Activity;
import android.graphics.Bitmap;
import com.google.android.gms.ads.AdListener;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.InterstitialAd;
import com.lody.virtual.GmsSupport;
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.os.VUserInfo;
import com.lody.virtual.os.VUserManager;
import com.lody.virtual.remote.InstallResult;
import com.lody.virtual.remote.InstalledAppInfo;
import java.io.IOException;
import io.virtualapp.VCommends;
import io.virtualapp.abs.ui.VUiKit;
import io.virtualapp.home.ads.AdScheduler;
import io.virtualapp.home.models.AppData;
import io.virtualapp.home.models.AppInfoLite;
import io.virtualapp.home.models.MultiplePackageAppData;
import io.virtualapp.home.models.PackageAppData;
import io.virtualapp.home.repo.AppRepository;
import io.virtualapp.home.repo.PackageAppDataStorage;
import jonathanfinerty.once.Once;
/**
* @author Lody
*/
class HomePresenterImpl implements HomeContract.HomePresenter {
private HomeContract.HomeView mView;
private Activity mActivity;
private AppRepository mRepo;
private InterstitialAd mInterstitialAd;
private AdScheduler mScheduler = new AdScheduler(10000L);
private AppData mTempAppData;
HomePresenterImpl(HomeContract.HomeView view) {
mView = view;
mActivity = view.getActivity();
mRepo = new AppRepository(mActivity);
mView.setPresenter(this);
mInterstitialAd = new InterstitialAd(mActivity);
mInterstitialAd.setAdUnitId("ca-app-pub-1609791120068944/6903216910");
mInterstitialAd.loadAd(new AdRequest.Builder().build());
mInterstitialAd.setAdListener(new AdListener() {
@Override
public void onAdClosed() {
if (mTempAppData != null) {
launchApp(mTempAppData);
mTempAppData = null;
}
mInterstitialAd.loadAd(new AdRequest.Builder().build());
}
});
}
@Override
public void start() {
dataChanged();
if (!Once.beenDone(VCommends.TAG_SHOW_ADD_APP_GUIDE)) {
mView.showGuide();
Once.markDone(VCommends.TAG_SHOW_ADD_APP_GUIDE);
}
if (!Once.beenDone(VCommends.TAG_ASK_INSTALL_GMS) && GmsSupport.isOutsideGoogleFrameworkExist()) {
mView.askInstallGms();
Once.markDone(VCommends.TAG_ASK_INSTALL_GMS);
}
}
@Override
public void launchApp(AppData data) {
if (mInterstitialAd.isLoaded() && mScheduler.shouldShowAd()) {
mTempAppData = data;
mInterstitialAd.show();
mScheduler.adShowed();
} else {
launchAppNoAd(data);
}
}
public void launchAppNoAd(AppData data) {
try {
if (data instanceof PackageAppData) {
PackageAppData appData = (PackageAppData) data;
appData.isFirstOpen = false;
LoadingActivity.launch(mActivity, appData.packageName, 0);
} else if (data instanceof MultiplePackageAppData) {
MultiplePackageAppData multipleData = (MultiplePackageAppData) data;
multipleData.isFirstOpen = false;
LoadingActivity.launch(mActivity, multipleData.appInfo.packageName, ((MultiplePackageAppData) data).userId);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
@Override
public void dataChanged() {
mView.showLoading();
mRepo.getVirtualApps().done(mView::loadFinish).fail(mView::loadError);
}
@Override
public void addApp(AppInfoLite info) {
class AddResult {
private PackageAppData appData;
private int userId;
private boolean justEnableHidden;
}
AddResult addResult = new AddResult();
VUiKit.defer().when(() -> {
InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(info.packageName, 0);
addResult.justEnableHidden = installedAppInfo != null;
if (addResult.justEnableHidden) {
int[] userIds = installedAppInfo.getInstalledUsers();
int nextUserId = userIds.length;
/*
Input : userIds = {0, 1, 3}
Output: nextUserId = 2
*/
for (int i = 0; i < userIds.length; i++) {
if (userIds[i] != i) {
nextUserId = i;
break;
}
}
addResult.userId = nextUserId;
if (VUserManager.get().getUserInfo(nextUserId) == null) {
// user not exist, create it automatically.
String nextUserName = "Space " + (nextUserId + 1);
VUserInfo newUserInfo = VUserManager.get().createUser(nextUserName, VUserInfo.FLAG_ADMIN);
if (newUserInfo == null) {
throw new IllegalStateException();
}
}
boolean success = VirtualCore.get().installPackageAsUser(nextUserId, info.packageName);
if (!success) {
throw new IllegalStateException();
}
} else {
InstallResult res = mRepo.addVirtualApp(info);
if (!res.isSuccess) {
throw new IllegalStateException();
}
}
}).then((res) -> {
addResult.appData = PackageAppDataStorage.get().acquire(info.packageName);
}).done(res -> {
boolean multipleVersion = addResult.justEnableHidden && addResult.userId != 0;
if (!multipleVersion) {
PackageAppData data = addResult.appData;
data.isLoading = true;
mView.addAppToLauncher(data);
handleOptApp(data, info.packageName, true);
} else {
MultiplePackageAppData data = new MultiplePackageAppData(addResult.appData, addResult.userId);
data.isLoading = true;
mView.addAppToLauncher(data);
handleOptApp(data, info.packageName, false);
}
});
}
private void handleOptApp(AppData data, String packageName, boolean needOpt) {
VUiKit.defer().when(() -> {
long time = System.currentTimeMillis();
if (needOpt) {
try {
VirtualCore.get().preOpt(packageName);
} catch (IOException e) {
e.printStackTrace();
}
}
time = System.currentTimeMillis() - time;
if (time < 1500L) {
try {
Thread.sleep(1500L - time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).done((res) -> {
if (data instanceof PackageAppData) {
((PackageAppData) data).isLoading = false;
((PackageAppData) data).isFirstOpen = true;
} else if (data instanceof MultiplePackageAppData) {
((MultiplePackageAppData) data).isLoading = false;
((MultiplePackageAppData) data).isFirstOpen = true;
}
mView.refreshLauncherItem(data);
});
}
@Override
public void deleteApp(AppData data) {
try {
mView.removeAppToLauncher(data);
if (data instanceof PackageAppData) {
mRepo.removeVirtualApp(((PackageAppData) data).packageName, 0);
} else {
MultiplePackageAppData appData = (MultiplePackageAppData) data;
mRepo.removeVirtualApp(appData.appInfo.packageName, appData.userId);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
@Override
public void createShortcut(AppData data) {
VirtualCore.OnEmitShortcutListener listener = new VirtualCore.OnEmitShortcutListener() {
@Override
public Bitmap getIcon(Bitmap originIcon) {
return originIcon;
}
@Override
public String getName(String originName) {
return originName + "(VA)";
}
};
if (data instanceof PackageAppData) {
VirtualCore.get().createShortcut(0, ((PackageAppData) data).packageName, listener);
} else if (data instanceof MultiplePackageAppData) {
MultiplePackageAppData appData = (MultiplePackageAppData) data;
VirtualCore.get().createShortcut(appData.userId, appData.appInfo.packageName, listener);
}
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/ListAppActivity.java
================================================
package io.virtualapp.home;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.v4.app.ActivityCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import io.virtualapp.R;
import io.virtualapp.VCommends;
import io.virtualapp.abs.ui.VActivity;
import io.virtualapp.home.adapters.AppPagerAdapter;
/**
* @author Lody
*/
public class ListAppActivity extends VActivity {
private Toolbar mToolBar;
private TabLayout mTabLayout;
private ViewPager mViewPager;
public static void gotoListApp(Activity activity) {
Intent intent = new Intent(activity, ListAppActivity.class);
activity.startActivityForResult(intent, VCommends.REQUEST_SELECT_APP);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.colorPrimaryDark)));
setContentView(R.layout.activity_clone_app);
mToolBar = (Toolbar) findViewById(R.id.clone_app_tool_bar);
mTabLayout = (TabLayout) mToolBar.findViewById(R.id.clone_app_tab_layout);
mViewPager = (ViewPager) findViewById(R.id.clone_app_view_pager);
setupToolBar();
mViewPager.setAdapter(new AppPagerAdapter(getSupportFragmentManager()));
mTabLayout.setupWithViewPager(mViewPager);
// Request permission to access external storage
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
}
}
}
private void setupToolBar() {
setSupportActionBar(mToolBar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
for (int result : grantResults) {
if (result == PackageManager.PERMISSION_GRANTED) {
mViewPager.setAdapter(new AppPagerAdapter(getSupportFragmentManager()));
break;
}
}
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/ListAppContract.java
================================================
package io.virtualapp.home;
import java.util.List;
import io.virtualapp.abs.BasePresenter;
import io.virtualapp.abs.BaseView;
import io.virtualapp.home.models.AppInfo;
/**
* @author Lody
* @version 1.0
*/
/*package*/ class ListAppContract {
interface ListAppView extends BaseView {
void startLoading();
void loadFinish(List infoList);
}
interface ListAppPresenter extends BasePresenter {
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/ListAppFragment.java
================================================
package io.virtualapp.home;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.widget.OrientationHelper;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import io.virtualapp.R;
import io.virtualapp.VCommends;
import io.virtualapp.abs.ui.VFragment;
import io.virtualapp.abs.ui.VUiKit;
import io.virtualapp.home.adapters.CloneAppListAdapter;
import io.virtualapp.home.adapters.decorations.ItemOffsetDecoration;
import io.virtualapp.home.models.AppInfo;
import io.virtualapp.home.models.AppInfoLite;
import io.virtualapp.widgets.DragSelectRecyclerView;
/**
* @author Lody
*/
public class ListAppFragment extends VFragment implements ListAppContract.ListAppView {
private static final String KEY_SELECT_FROM = "key_select_from";
private DragSelectRecyclerView mRecyclerView;
private ProgressBar mProgressBar;
private Button mInstallButton;
private CloneAppListAdapter mAdapter;
public static ListAppFragment newInstance(File selectFrom) {
Bundle args = new Bundle();
if (selectFrom != null)
args.putString(KEY_SELECT_FROM, selectFrom.getPath());
ListAppFragment fragment = new ListAppFragment();
fragment.setArguments(args);
return fragment;
}
private File getSelectFrom() {
Bundle bundle = getArguments();
if (bundle != null) {
String selectFrom = bundle.getString(KEY_SELECT_FROM);
if (selectFrom != null) {
return new File(selectFrom);
}
}
return null;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_list_app, null);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mAdapter.saveInstanceState(outState);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mRecyclerView = (DragSelectRecyclerView) view.findViewById(R.id.select_app_recycler_view);
mProgressBar = (ProgressBar) view.findViewById(R.id.select_app_progress_bar);
mInstallButton = (Button) view.findViewById(R.id.select_app_install_btn);
mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, OrientationHelper.VERTICAL));
mRecyclerView.addItemDecoration(new ItemOffsetDecoration(VUiKit.dpToPx(getContext(), 2)));
mAdapter = new CloneAppListAdapter(getActivity());
mRecyclerView.setAdapter(mAdapter);
mAdapter.setOnItemClickListener(new CloneAppListAdapter.ItemEventListener() {
@Override
public void onItemClick(AppInfo info, int position) {
int count = mAdapter.getSelectedCount();
if (!mAdapter.isIndexSelected(position)) {
if (count >= 9) {
Toast.makeText(getContext(), R.string.install_too_much_once_time, Toast.LENGTH_SHORT).show();
return;
}
}
mAdapter.toggleSelected(position);
}
@Override
public boolean isSelectable(int position) {
return mAdapter.isIndexSelected(position) || mAdapter.getSelectedCount() < 9;
}
});
mAdapter.setSelectionListener(count -> {
mInstallButton.setEnabled(count > 0);
mInstallButton.setText(String.format(Locale.ENGLISH, getResources().getString(R.string.install_d), count));
});
mInstallButton.setOnClickListener(v -> {
Integer[] selectedIndices = mAdapter.getSelectedIndices();
ArrayList dataList = new ArrayList(selectedIndices.length);
for (int index : selectedIndices) {
AppInfo info = mAdapter.getItem(index);
dataList.add(new AppInfoLite(info.packageName, info.path, info.fastOpen));
}
Intent data = new Intent();
data.putParcelableArrayListExtra(VCommends.EXTRA_APP_INFO_LIST, dataList);
getActivity().setResult(Activity.RESULT_OK, data);
getActivity().finish();
});
new ListAppPresenterImpl(getActivity(), this, getSelectFrom()).start();
}
@Override
public void startLoading() {
mProgressBar.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
}
@Override
public void loadFinish(List infoList) {
mAdapter.setList(infoList);
mRecyclerView.setDragSelectActive(true, 0);
mAdapter.setSelected(0, false);
mProgressBar.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
}
@Override
public void setPresenter(ListAppContract.ListAppPresenter presenter) {
this.mPresenter = presenter;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/ListAppPresenterImpl.java
================================================
package io.virtualapp.home;
import android.app.Activity;
import android.content.Intent;
import java.io.File;
import io.virtualapp.VCommends;
import io.virtualapp.home.repo.AppDataSource;
import io.virtualapp.home.models.PackageAppData;
import io.virtualapp.home.repo.AppRepository;
/**
* @author Lody
*/
class ListAppPresenterImpl implements ListAppContract.ListAppPresenter {
private Activity mActivity;
private ListAppContract.ListAppView mView;
private AppDataSource mRepository;
private File from;
ListAppPresenterImpl(Activity activity, ListAppContract.ListAppView view, File fromWhere) {
mActivity = activity;
mView = view;
mRepository = new AppRepository(activity);
mView.setPresenter(this);
this.from = fromWhere;
}
@Override
public void start() {
mView.setPresenter(this);
mView.startLoading();
if (from == null)
mRepository.getInstalledApps(mActivity).done(mView::loadFinish);
else
mRepository.getStorageApps(mActivity, from).done(mView::loadFinish);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/LoadingActivity.java
================================================
package io.virtualapp.home;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.RemoteException;
import android.widget.ImageView;
import android.widget.TextView;
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.client.ipc.VActivityManager;
import java.util.Locale;
import io.virtualapp.R;
import io.virtualapp.abs.ui.VActivity;
import io.virtualapp.abs.ui.VUiKit;
import io.virtualapp.home.models.PackageAppData;
import io.virtualapp.home.repo.PackageAppDataStorage;
import io.virtualapp.widgets.EatBeansView;
/**
* @author Lody
*/
public class LoadingActivity extends VActivity {
private static final String PKG_NAME_ARGUMENT = "MODEL_ARGUMENT";
private static final String KEY_INTENT = "KEY_INTENT";
private static final String KEY_USER = "KEY_USER";
private PackageAppData appModel;
private EatBeansView loadingView;
public static void launch(Context context, String packageName, int userId) {
Intent intent = VirtualCore.get().getLaunchIntent(packageName, userId);
if (intent != null) {
Intent loadingPageIntent = new Intent(context, LoadingActivity.class);
loadingPageIntent.putExtra(PKG_NAME_ARGUMENT, packageName);
loadingPageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
loadingPageIntent.putExtra(KEY_INTENT, intent);
loadingPageIntent.putExtra(KEY_USER, userId);
context.startActivity(loadingPageIntent);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_loading);
loadingView = (EatBeansView) findViewById(R.id.loading_anim);
int userId = getIntent().getIntExtra(KEY_USER, -1);
String pkg = getIntent().getStringExtra(PKG_NAME_ARGUMENT);
appModel = PackageAppDataStorage.get().acquire(pkg);
ImageView iconView = (ImageView) findViewById(R.id.app_icon);
iconView.setImageDrawable(appModel.icon);
TextView nameView = (TextView) findViewById(R.id.app_name);
nameView.setText(String.format(Locale.ENGLISH, "Opening %s...", appModel.name));
Intent intent = getIntent().getParcelableExtra(KEY_INTENT);
if (intent == null) {
return;
}
VirtualCore.get().setUiCallback(intent, mUiCallback);
VUiKit.defer().when(() -> {
long startTime = System.currentTimeMillis();
if (!appModel.fastOpen) {
try {
VirtualCore.get().preOpt(appModel.packageName);
} catch (Exception e) {
e.printStackTrace();
}
}
long spend = System.currentTimeMillis() - startTime;
if (spend < 500) {
try {
Thread.sleep(500 - spend);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
VActivityManager.get().startActivity(intent, userId);
});
}
private final VirtualCore.UiCallback mUiCallback = new VirtualCore.UiCallback() {
@Override
public void onAppOpened(String packageName, int userId) throws RemoteException {
finish();
}
};
@Override
protected void onResume() {
super.onResume();
loadingView.startAnim();
}
@Override
protected void onPause() {
super.onPause();
loadingView.stopAnim();
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/adapters/AppPagerAdapter.java
================================================
package io.virtualapp.home.adapters;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import com.lody.virtual.helper.utils.Reflect;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import io.virtualapp.R;
import io.virtualapp.VApp;
import io.virtualapp.home.ListAppFragment;
/**
* @author Lody
*/
public class AppPagerAdapter extends FragmentPagerAdapter {
private List titles = new ArrayList<>();
private List dirs = new ArrayList<>();
public AppPagerAdapter(FragmentManager fm) {
super(fm);
titles.add(VApp.getApp().getResources().getString(R.string.clone_apps));
dirs.add(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Context ctx = VApp.getApp();
StorageManager storage = (StorageManager) ctx.getSystemService(Context.STORAGE_SERVICE);
for (StorageVolume volume : storage.getStorageVolumes()) {
//Why the fuck are getPathFile and getUserLabel hidden?!
//StorageVolume is kinda useless without those...
File dir = Reflect.on(volume).call("getPathFile").get();
String label = Reflect.on(volume).call("getUserLabel").get();
if (dir.listFiles() != null) {
titles.add(label);
dirs.add(dir);
}
}
} else {
// Fallback: only support the default storage sources
File storageFir = Environment.getExternalStorageDirectory();
if (storageFir != null && storageFir.isDirectory()) {
titles.add(VApp.getApp().getResources().getString(R.string.external_storage));
dirs.add(storageFir);
}
}
}
@Override
public Fragment getItem(int position) {
return ListAppFragment.newInstance(dirs.get(position));
}
@Override
public int getCount() {
return titles.size();
}
@Override
public CharSequence getPageTitle(int position) {
return titles.get(position);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/adapters/CloneAppListAdapter.java
================================================
package io.virtualapp.home.adapters;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
import io.virtualapp.R;
import io.virtualapp.abs.ui.VUiKit;
import io.virtualapp.home.models.AppInfo;
import io.virtualapp.widgets.DragSelectRecyclerViewAdapter;
import io.virtualapp.widgets.LabelView;
/**
* @author Lody
*/
public class CloneAppListAdapter extends DragSelectRecyclerViewAdapter {
private static final int TYPE_FOOTER = -2;
private final View mFooterView;
private LayoutInflater mInflater;
private List mAppList;
private ItemEventListener mItemEventListener;
public CloneAppListAdapter(Context context) {
this.mInflater = LayoutInflater.from(context);
mFooterView = new View(context);
StaggeredGridLayoutManager.LayoutParams params = new StaggeredGridLayoutManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, VUiKit.dpToPx(context, 60)
);
params.setFullSpan(true);
mFooterView.setLayoutParams(params);
}
public void setOnItemClickListener(ItemEventListener mItemEventListener) {
this.mItemEventListener = mItemEventListener;
}
public List getList() {
return mAppList;
}
public void setList(List models) {
this.mAppList = models;
notifyDataSetChanged();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_FOOTER) {
return new ViewHolder(mFooterView);
}
return new ViewHolder(mInflater.inflate(R.layout.item_clone_app, null));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
if (getItemViewType(position) == TYPE_FOOTER) {
return;
}
super.onBindViewHolder(holder, position);
AppInfo info = mAppList.get(position);
holder.iconView.setImageDrawable(info.icon);
holder.nameView.setText(info.name);
if (isIndexSelected(position)) {
holder.iconView.setAlpha(1f);
holder.appCheckView.setImageResource(R.drawable.ic_check);
} else {
holder.iconView.setAlpha(0.65f);
holder.appCheckView.setImageResource(R.drawable.ic_no_check);
}
if (info.cloneCount > 0) {
holder.labelView.setVisibility(View.VISIBLE);
holder.labelView.setText(info.cloneCount + 1 + "");
} else {
holder.labelView.setVisibility(View.INVISIBLE);
}
holder.itemView.setOnClickListener(v -> {
mItemEventListener.onItemClick(info, position);
});
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
}
@Override
protected boolean isIndexSelectable(int index) {
return mItemEventListener.isSelectable(index);
}
@Override
public int getItemCount() {
return mAppList == null ? 1 : mAppList.size() + 1;
}
@Override
public int getItemViewType(int position) {
if (position == getItemCount() - 1) {
return TYPE_FOOTER;
}
return super.getItemViewType(position);
}
public AppInfo getItem(int index) {
return mAppList.get(index);
}
public interface ItemEventListener {
void onItemClick(AppInfo appData, int position);
boolean isSelectable(int position);
}
class ViewHolder extends RecyclerView.ViewHolder {
private ImageView iconView;
private TextView nameView;
private ImageView appCheckView;
private LabelView labelView;
ViewHolder(View itemView) {
super(itemView);
if (itemView != mFooterView) {
iconView = (ImageView) itemView.findViewById(R.id.item_app_icon);
nameView = (TextView) itemView.findViewById(R.id.item_app_name);
appCheckView = (ImageView) itemView.findViewById(R.id.item_app_checked);
labelView = (LabelView) itemView.findViewById(R.id.item_app_clone_count);
}
}
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/adapters/LaunchpadAdapter.java
================================================
package io.virtualapp.home.adapters;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.SparseIntArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
import io.virtualapp.R;
import io.virtualapp.abs.ui.VUiKit;
import io.virtualapp.home.models.AppData;
import io.virtualapp.home.models.MultiplePackageAppData;
import io.virtualapp.widgets.LabelView;
import io.virtualapp.widgets.LauncherIconView;
/**
* @author Lody
*/
public class LaunchpadAdapter extends RecyclerView.Adapter {
private LayoutInflater mInflater;
private List mList;
private SparseIntArray mColorArray = new SparseIntArray();
private OnAppClickListener mAppClickListener;
public LaunchpadAdapter(Context context) {
mInflater = LayoutInflater.from(context);
}
public void add(AppData model) {
int insertPos = mList.size() - 1;
mList.add(insertPos, model);
notifyItemInserted(insertPos);
}
public void replace(int index, AppData data) {
mList.set(index, data);
notifyItemChanged(index);
}
public void remove(AppData data) {
if (mList.remove(data)) {
notifyDataSetChanged();
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(mInflater.inflate(R.layout.item_launcher_app, null));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
AppData data = mList.get(position);
holder.color = getColor(position);
holder.iconView.setImageDrawable(data.getIcon());
holder.nameView.setText(data.getName());
if (data.isFirstOpen() && !data.isLoading()) {
holder.firstOpenDot.setVisibility(View.VISIBLE);
} else {
holder.firstOpenDot.setVisibility(View.INVISIBLE);
}
holder.itemView.setBackgroundColor(holder.color);
holder.itemView.setOnClickListener(v -> {
if (mAppClickListener != null) {
mAppClickListener.onAppClick(position, data);
}
});
if (data instanceof MultiplePackageAppData) {
MultiplePackageAppData multipleData = (MultiplePackageAppData) data;
holder.spaceLabelView.setVisibility(View.VISIBLE);
holder.spaceLabelView.setText(multipleData.userId + 1 + "");
} else {
holder.spaceLabelView.setVisibility(View.INVISIBLE);
}
if (data.isLoading()) {
startLoadingAnimation(holder.iconView);
} else {
holder.iconView.setProgress(100, false);
}
}
private void startLoadingAnimation(LauncherIconView iconView) {
iconView.setProgress(40, true);
VUiKit.defer().when(() -> {
try {
Thread.sleep(900L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).done((res) -> iconView.setProgress(80, true));
}
private int getColor(int position) {
int color = mColorArray.get(position);
if (color == 0) {
int type = position % 3;
int row = position / 3;
int rowType = row % 3;
if (rowType == 0) {
if (type == 0) {
color = mInflater.getContext().getResources().getColor(R.color.desktopColorA);
} else if (type == 1) {
color = mInflater.getContext().getResources().getColor(R.color.desktopColorB);
} else {
color = mInflater.getContext().getResources().getColor(R.color.desktopColorC);
}
} else if (rowType == 1) {
if (type == 0) {
color = mInflater.getContext().getResources().getColor(R.color.desktopColorB);
} else if (type == 1) {
color = mInflater.getContext().getResources().getColor(R.color.desktopColorC);
} else {
color = mInflater.getContext().getResources().getColor(R.color.desktopColorA);
}
} else {
if (type == 0) {
color = mInflater.getContext().getResources().getColor(R.color.desktopColorC);
} else if (type == 1) {
color = mInflater.getContext().getResources().getColor(R.color.desktopColorA);
} else {
color = mInflater.getContext().getResources().getColor(R.color.desktopColorB);
}
}
mColorArray.put(position, color);
}
return color;
}
@Override
public int getItemCount() {
return mList == null ? 0 : mList.size();
}
public List getList() {
return mList;
}
public void setList(List list) {
this.mList = list;
notifyDataSetChanged();
}
public void setAppClickListener(OnAppClickListener mAppClickListener) {
this.mAppClickListener = mAppClickListener;
}
public void moveItem(int pos, int targetPos) {
AppData model = mList.remove(pos);
mList.add(targetPos, model);
notifyItemMoved(pos, targetPos);
}
public void refresh(AppData model) {
int index = mList.indexOf(model);
if (index >= 0) {
notifyItemChanged(index);
}
}
public interface OnAppClickListener {
void onAppClick(int position, AppData model);
}
public class ViewHolder extends RecyclerView.ViewHolder {
public int color;
LauncherIconView iconView;
TextView nameView;
LabelView spaceLabelView;
View firstOpenDot;
ViewHolder(View itemView) {
super(itemView);
iconView = (LauncherIconView) itemView.findViewById(R.id.item_app_icon);
nameView = (TextView) itemView.findViewById(R.id.item_app_name);
spaceLabelView = (LabelView) itemView.findViewById(R.id.item_app_space_idx);
firstOpenDot = itemView.findViewById(R.id.item_first_open_dot);
}
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/adapters/decorations/ItemOffsetDecoration.java
================================================
package io.virtualapp.home.adapters.decorations;
import android.content.Context;
import android.graphics.Rect;
import android.support.annotation.DimenRes;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.View;
public class ItemOffsetDecoration extends RecyclerView.ItemDecoration {
private int mItemOffset;
public ItemOffsetDecoration(int itemOffset) {
mItemOffset = itemOffset;
}
public ItemOffsetDecoration(@NonNull Context context, @DimenRes int itemOffsetId) {
this(context.getResources().getDimensionPixelSize(itemOffsetId));
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
outRect.set(mItemOffset, mItemOffset, mItemOffset, mItemOffset);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/ads/AdScheduler.java
================================================
package io.virtualapp.home.ads;
/**
* @author Lody
*/
public class AdScheduler {
private long adDeltaTime;
private long lastShowAdTime;
public AdScheduler(long adDeltaTime) {
this.adDeltaTime = adDeltaTime;
}
public void adShowed() {
lastShowAdTime = System.currentTimeMillis();
}
public boolean shouldShowAd() {
return System.currentTimeMillis() - lastShowAdTime >= adDeltaTime;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/models/AddAppButton.java
================================================
package io.virtualapp.home.models;
import android.content.Context;
import android.graphics.drawable.Drawable;
import io.virtualapp.R;
/**
* @author Lody
*/
public class AddAppButton implements AppData {
private String name;
private Drawable icon;
public AddAppButton(Context context) {
name = context.getResources().getString(R.string.add_app);
icon = context.getResources().getDrawable(R.drawable.ic_add_circle);
}
@Override
public boolean isLoading() {
return false;
}
@Override
public boolean isFirstOpen() {
return false;
}
@Override
public Drawable getIcon() {
return icon;
}
@Override
public String getName() {
return name;
}
@Override
public boolean canReorder() {
return false;
}
@Override
public boolean canLaunch() {
return false;
}
@Override
public boolean canDelete() {
return false;
}
@Override
public boolean canCreateShortcut() {
return false;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/models/AppData.java
================================================
package io.virtualapp.home.models;
import android.graphics.drawable.Drawable;
/**
* @author Lody
*/
public interface AppData {
boolean isLoading();
boolean isFirstOpen();
Drawable getIcon();
String getName();
boolean canReorder();
boolean canLaunch();
boolean canDelete();
boolean canCreateShortcut();
}
================================================
FILE: app/src/main/java/io/virtualapp/home/models/AppInfo.java
================================================
package io.virtualapp.home.models;
import android.graphics.drawable.Drawable;
/**
* @author Lody
*/
public class AppInfo {
public String packageName;
public String path;
public boolean fastOpen;
public Drawable icon;
public CharSequence name;
public int cloneCount;
}
================================================
FILE: app/src/main/java/io/virtualapp/home/models/AppInfoLite.java
================================================
package io.virtualapp.home.models;
import android.os.Parcel;
import android.os.Parcelable;
/**
* @author Lody
*/
public class AppInfoLite implements Parcelable {
public static final Creator CREATOR = new Creator() {
@Override
public AppInfoLite createFromParcel(Parcel source) {
return new AppInfoLite(source);
}
@Override
public AppInfoLite[] newArray(int size) {
return new AppInfoLite[size];
}
};
public String packageName;
public String path;
public boolean fastOpen;
public AppInfoLite(String packageName, String path, boolean fastOpen) {
this.packageName = packageName;
this.path = path;
this.fastOpen = fastOpen;
}
protected AppInfoLite(Parcel in) {
this.packageName = in.readString();
this.path = in.readString();
this.fastOpen = in.readByte() != 0;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.packageName);
dest.writeString(this.path);
dest.writeByte(this.fastOpen ? (byte) 1 : (byte) 0);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/models/EmptyAppData.java
================================================
package io.virtualapp.home.models;
import android.graphics.drawable.Drawable;
/**
* @author Lody
*/
public class EmptyAppData implements AppData {
@Override
public boolean isLoading() {
return false;
}
@Override
public boolean isFirstOpen() {
return false;
}
@Override
public Drawable getIcon() {
return null;
}
@Override
public String getName() {
return null;
}
@Override
public boolean canReorder() {
return false;
}
@Override
public boolean canLaunch() {
return false;
}
@Override
public boolean canDelete() {
return false;
}
@Override
public boolean canCreateShortcut() {
return false;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/models/MultiplePackageAppData.java
================================================
package io.virtualapp.home.models;
import android.graphics.drawable.Drawable;
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.remote.InstalledAppInfo;
/**
* @author Lody
*/
public class MultiplePackageAppData implements AppData {
public InstalledAppInfo appInfo;
public int userId;
public boolean isFirstOpen;
public boolean isLoading;
public Drawable icon;
public String name;
public MultiplePackageAppData(PackageAppData target, int userId) {
this.userId = userId;
this.appInfo = VirtualCore.get().getInstalledAppInfo(target.packageName, 0);
this.isFirstOpen = !appInfo.isLaunched(userId);
if (target.icon != null) {
Drawable.ConstantState state = target.icon.getConstantState();
if (state != null) {
icon = state.newDrawable();
}
}
name = target.name;
}
@Override
public boolean isLoading() {
return isLoading;
}
@Override
public boolean isFirstOpen() {
return isFirstOpen;
}
@Override
public Drawable getIcon() {
return icon;
}
@Override
public String getName() {
return name;
}
@Override
public boolean canReorder() {
return true;
}
@Override
public boolean canLaunch() {
return true;
}
@Override
public boolean canDelete() {
return true;
}
@Override
public boolean canCreateShortcut() {
return true;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/models/PackageAppData.java
================================================
package io.virtualapp.home.models;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import com.lody.virtual.remote.InstalledAppInfo;
/**
* @author Lody
*/
public class PackageAppData implements AppData {
public String packageName;
public String name;
public Drawable icon;
public boolean fastOpen;
public boolean isFirstOpen;
public boolean isLoading;
public PackageAppData(Context context, InstalledAppInfo installedAppInfo) {
this.packageName = installedAppInfo.packageName;
this.isFirstOpen = !installedAppInfo.isLaunched(0);
loadData(context, installedAppInfo.getApplicationInfo(installedAppInfo.getInstalledUsers()[0]));
}
private void loadData(Context context, ApplicationInfo appInfo) {
if (appInfo == null) {
return;
}
PackageManager pm = context.getPackageManager();
try {
CharSequence sequence = appInfo.loadLabel(pm);
if (sequence != null) {
name = sequence.toString();
}
icon = appInfo.loadIcon(pm);
} catch (Throwable e) {
e.printStackTrace();
}
}
@Override
public boolean isLoading() {
return isLoading;
}
@Override
public boolean isFirstOpen() {
return isFirstOpen;
}
@Override
public Drawable getIcon() {
return icon;
}
@Override
public String getName() {
return name;
}
@Override
public boolean canReorder() {
return true;
}
@Override
public boolean canLaunch() {
return true;
}
@Override
public boolean canDelete() {
return true;
}
@Override
public boolean canCreateShortcut() {
return true;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/platform/PlatformInfo.java
================================================
package io.virtualapp.home.platform;
import android.content.pm.PackageInfo;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* @author Lody
*/
public abstract class PlatformInfo {
private final Set platformPkgs = new HashSet<>();
public PlatformInfo(String... pkgs) {
Collections.addAll(platformPkgs, pkgs);
}
public abstract boolean relyOnPlatform(PackageInfo info);
}
================================================
FILE: app/src/main/java/io/virtualapp/home/platform/WechatPlatformInfo.java
================================================
package io.virtualapp.home.platform;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
/**
* @author Lody
*/
public class WechatPlatformInfo extends PlatformInfo {
public WechatPlatformInfo() {
super("com.tencent.mm");
}
@Override
public boolean relyOnPlatform(PackageInfo info) {
if (info.activities == null) {
return false;
}
for (ActivityInfo activityInfo : info.activities) {
if (activityInfo.name.endsWith("WXEntryActivity")) {
return true;
}
}
return false;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/repo/AppDataSource.java
================================================
package io.virtualapp.home.repo;
import android.content.Context;
import com.lody.virtual.remote.InstallResult;
import org.jdeferred.Promise;
import java.io.File;
import java.util.List;
import io.virtualapp.home.models.AppData;
import io.virtualapp.home.models.AppInfo;
import io.virtualapp.home.models.AppInfoLite;
/**
* @author Lody
* @version 1.0
*/
public interface AppDataSource {
/**
* @return All the Applications we Virtual.
*/
Promise, Throwable, Void> getVirtualApps();
/**
* @param context Context
* @return All the Applications we Installed.
*/
Promise, Throwable, Void> getInstalledApps(Context context);
Promise, Throwable, Void> getStorageApps(Context context, File rootDir);
InstallResult addVirtualApp(AppInfoLite info);
boolean removeVirtualApp(String packageName, int userId);
}
================================================
FILE: app/src/main/java/io/virtualapp/home/repo/AppRepository.java
================================================
package io.virtualapp.home.repo;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import com.lody.virtual.GmsSupport;
import com.lody.virtual.client.core.InstallStrategy;
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.remote.InstallResult;
import com.lody.virtual.remote.InstalledAppInfo;
import org.jdeferred.Promise;
import java.io.File;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import io.virtualapp.abs.ui.VUiKit;
import io.virtualapp.home.models.AppData;
import io.virtualapp.home.models.AppInfo;
import io.virtualapp.home.models.AppInfoLite;
import io.virtualapp.home.models.MultiplePackageAppData;
import io.virtualapp.home.models.PackageAppData;
/**
* @author Lody
*/
public class AppRepository implements AppDataSource {
private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA);
private static final List SCAN_PATH_LIST = Arrays.asList(
".",
"wandoujia/app",
"tencent/tassistant/apk",
"BaiduAsa9103056",
"360Download",
"pp/downloader",
"pp/downloader/apk",
"pp/downloader/silent/apk");
private Context mContext;
public AppRepository(Context context) {
mContext = context;
}
private static boolean isSystemApplication(PackageInfo packageInfo) {
return (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
&& !GmsSupport.isGmsFamilyPackage(packageInfo.packageName);
}
@Override
public Promise, Throwable, Void> getVirtualApps() {
return VUiKit.defer().when(() -> {
List infos = VirtualCore.get().getInstalledApps(0);
List models = new ArrayList<>();
for (InstalledAppInfo info : infos) {
if (!VirtualCore.get().isPackageLaunchable(info.packageName)) {
continue;
}
PackageAppData data = new PackageAppData(mContext, info);
if (VirtualCore.get().isAppInstalledAsUser(0, info.packageName)) {
models.add(data);
}
int[] userIds = info.getInstalledUsers();
for (int userId : userIds) {
if (userId != 0) {
models.add(new MultiplePackageAppData(data, userId));
}
}
}
return models;
});
}
@Override
public Promise, Throwable, Void> getInstalledApps(Context context) {
return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, context.getPackageManager().getInstalledPackages(0), true));
}
@Override
public Promise, Throwable, Void> getStorageApps(Context context, File rootDir) {
return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, findAndParseAPKs(context, rootDir, SCAN_PATH_LIST), false));
}
private List findAndParseAPKs(Context context, File rootDir, List paths) {
List packageList = new ArrayList<>();
if (paths == null)
return packageList;
for (String path : paths) {
File[] dirFiles = new File(rootDir, path).listFiles();
if (dirFiles == null)
continue;
for (File f : dirFiles) {
if (!f.getName().toLowerCase().endsWith(".apk"))
continue;
PackageInfo pkgInfo = null;
try {
pkgInfo = context.getPackageManager().getPackageArchiveInfo(f.getAbsolutePath(), 0);
pkgInfo.applicationInfo.sourceDir = f.getAbsolutePath();
pkgInfo.applicationInfo.publicSourceDir = f.getAbsolutePath();
} catch (Exception e) {
// Ignore
}
if (pkgInfo != null)
packageList.add(pkgInfo);
}
}
return packageList;
}
private List convertPackageInfoToAppData(Context context, List pkgList, boolean fastOpen) {
PackageManager pm = context.getPackageManager();
List list = new ArrayList<>(pkgList.size());
String hostPkg = VirtualCore.get().getHostPkg();
for (PackageInfo pkg : pkgList) {
// ignore the host package
if (hostPkg.equals(pkg.packageName)) {
continue;
}
// ignore the System package
if (isSystemApplication(pkg)) {
continue;
}
ApplicationInfo ai = pkg.applicationInfo;
String path = ai.publicSourceDir != null ? ai.publicSourceDir : ai.sourceDir;
if (path == null) {
continue;
}
AppInfo info = new AppInfo();
info.packageName = pkg.packageName;
info.fastOpen = fastOpen;
info.path = path;
info.icon = ai.loadIcon(pm);
info.name = ai.loadLabel(pm);
InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(pkg.packageName, 0);
if (installedAppInfo != null) {
info.cloneCount = installedAppInfo.getInstalledUsers().length;
}
list.add(info);
}
return list;
}
@Override
public InstallResult addVirtualApp(AppInfoLite info) {
int flags = InstallStrategy.COMPARE_VERSION | InstallStrategy.SKIP_DEX_OPT;
if (info.fastOpen) {
flags |= InstallStrategy.DEPEND_SYSTEM_IF_EXIST;
}
return VirtualCore.get().installPackage(info.path, flags);
}
@Override
public boolean removeVirtualApp(String packageName, int userId) {
return VirtualCore.get().uninstallPackageAsUser(packageName, userId);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/home/repo/PackageAppDataStorage.java
================================================
package io.virtualapp.home.repo;
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.remote.InstalledAppInfo;
import java.util.HashMap;
import java.util.Map;
import io.virtualapp.VApp;
import io.virtualapp.abs.Callback;
import io.virtualapp.abs.ui.VUiKit;
import io.virtualapp.home.models.PackageAppData;
/**
* @author Lody
*
* Cache the loaded PackageAppData.
*/
public class PackageAppDataStorage {
private static final PackageAppDataStorage STORAGE = new PackageAppDataStorage();
private final Map packageDataMap = new HashMap<>();
public static PackageAppDataStorage get() {
return STORAGE;
}
public PackageAppData acquire(String packageName) {
PackageAppData data;
synchronized (packageDataMap) {
data = packageDataMap.get(packageName);
if (data == null) {
data = loadAppData(packageName);
}
}
return data;
}
public void acquire(String packageName, Callback callback) {
VUiKit.defer()
.when(() -> acquire(packageName))
.done(callback::callback);
}
private PackageAppData loadAppData(String packageName) {
InstalledAppInfo setting = VirtualCore.get().getInstalledAppInfo(packageName, 0);
if (setting != null) {
PackageAppData data = new PackageAppData(VApp.getApp(), setting);
synchronized (packageDataMap) {
packageDataMap.put(packageName, data);
}
return data;
}
return null;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/splash/SplashActivity.java
================================================
package io.virtualapp.splash;
import android.os.Bundle;
import android.view.WindowManager;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.AdView;
import com.lody.virtual.client.core.VirtualCore;
import io.virtualapp.R;
import io.virtualapp.VCommends;
import io.virtualapp.abs.ui.VActivity;
import io.virtualapp.abs.ui.VUiKit;
import io.virtualapp.home.FlurryROMCollector;
import io.virtualapp.home.HomeActivity;
import jonathanfinerty.once.Once;
public class SplashActivity extends VActivity {
private AdView mAdView;
@Override
protected void onCreate(Bundle savedInstanceState) {
@SuppressWarnings("unused")
boolean enterGuide = !Once.beenDone(Once.THIS_APP_INSTALL, VCommends.TAG_NEW_VERSION);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
showBanner();
VUiKit.defer().when(() -> {
if (!Once.beenDone("collect_flurry")) {
FlurryROMCollector.startCollect();
Once.markDone("collect_flurry");
}
long time = System.currentTimeMillis();
doActionInThread();
time = System.currentTimeMillis() - time;
long delta = 3000L - time;
if (delta > 0) {
VUiKit.sleep(delta);
}
}).done((res) -> {
HomeActivity.goHome(this);
finish();
});
}
private void showBanner() {
mAdView = (AdView) findViewById(R.id.splash_banner);
AdRequest adRequest = new AdRequest.Builder().build();
mAdView.loadAd(adRequest);
}
private void doActionInThread() {
if (!VirtualCore.get().isEngineLaunched()) {
VirtualCore.get().waitForEngine();
}
}
}
================================================
FILE: app/src/main/java/io/virtualapp/vs/VSManagerActivity.java
================================================
package io.virtualapp.vs;
import io.virtualapp.abs.ui.VActivity;
/**
* @author Lody
*
*
*
*/
public class VSManagerActivity extends VActivity {
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/BallGridBeatIndicator.java
================================================
package io.virtualapp.widgets;
import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.graphics.Paint;
import java.util.ArrayList;
public class BallGridBeatIndicator extends Indicator {
private static final int ALPHA = 255;
private static final int[] ALPHAS = new int[]{ALPHA,
ALPHA,
ALPHA,
ALPHA,
ALPHA,
ALPHA,
ALPHA,
ALPHA,
ALPHA};
@Override
public void draw(Canvas canvas, Paint paint) {
float circleSpacing = 4;
float radius = (getWidth() - circleSpacing * 4) / 6;
float x = getWidth() / 2 - (radius * 2 + circleSpacing);
float y = getWidth() / 2 - (radius * 2 + circleSpacing);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
canvas.save();
float translateX = x + (radius * 2) * j + circleSpacing * j;
float translateY = y + (radius * 2) * i + circleSpacing * i;
canvas.translate(translateX, translateY);
paint.setAlpha(ALPHAS[3 * i + j]);
canvas.drawCircle(0, 0, radius, paint);
canvas.restore();
}
}
}
@Override
public ArrayList onCreateAnimators() {
ArrayList animators = new ArrayList<>();
int[] durations = {960, 930, 1190, 1130, 1340, 940, 1200, 820, 1190};
int[] delays = {360, 400, 680, 410, 710, -150, -120, 10, 320};
for (int i = 0; i < 9; i++) {
final int index = i;
ValueAnimator alphaAnim = ValueAnimator.ofInt(255, 168, 255);
alphaAnim.setDuration(durations[i]);
alphaAnim.setRepeatCount(-1);
alphaAnim.setStartDelay(delays[i]);
addUpdateListener(alphaAnim, animation -> {
ALPHAS[index] = (int) animation.getAnimatedValue();
postInvalidate();
});
animators.add(alphaAnim);
}
return animators;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/BallPulseIndicator.java
================================================
package io.virtualapp.widgets;
import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.graphics.Paint;
import java.util.ArrayList;
public class BallPulseIndicator extends Indicator {
public static final float SCALE = 1.0f;
//scale x ,y
private float[] scaleFloats = new float[]{SCALE,
SCALE,
SCALE};
@Override
public void draw(Canvas canvas, Paint paint) {
float circleSpacing = 4;
float radius = (Math.min(getWidth(), getHeight()) - circleSpacing * 2) / 6;
float x = getWidth() / 2 - (radius * 2 + circleSpacing);
float y = getHeight() / 2;
for (int i = 0; i < 3; i++) {
canvas.save();
float translateX = x + (radius * 2) * i + circleSpacing * i;
canvas.translate(translateX, y);
canvas.scale(scaleFloats[i], scaleFloats[i]);
canvas.drawCircle(0, 0, radius, paint);
canvas.restore();
}
}
@Override
public ArrayList onCreateAnimators() {
ArrayList animators = new ArrayList<>();
int[] delays = new int[]{120, 240, 360};
for (int i = 0; i < 3; i++) {
final int index = i;
ValueAnimator scaleAnim = ValueAnimator.ofFloat(1, 0.3f, 1);
scaleAnim.setDuration(750);
scaleAnim.setRepeatCount(-1);
scaleAnim.setStartDelay(delays[i]);
addUpdateListener(scaleAnim, animation -> {
scaleFloats[index] = (float) animation.getAnimatedValue();
postInvalidate();
});
animators.add(scaleAnim);
}
return animators;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/BaseView.java
================================================
package io.virtualapp.widgets;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;
public abstract class BaseView extends View {
public ValueAnimator valueAnimator;
public BaseView(Context context) {
this(context, null);
}
public BaseView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BaseView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
InitPaint();
}
public void startAnim() {
stopAnim();
startViewAnim(0f, 1f, 500);
}
public void startAnim(int time) {
stopAnim();
startViewAnim(0f, 1f, time);
}
public void stopAnim() {
if (valueAnimator != null) {
clearAnimation();
valueAnimator.setRepeatCount(0);
valueAnimator.cancel();
valueAnimator.end();
if (OnStopAnim() == 0) {
valueAnimator.setRepeatCount(0);
valueAnimator.cancel();
valueAnimator.end();
}
}
}
private ValueAnimator startViewAnim(float startF, final float endF, long time) {
valueAnimator = ValueAnimator.ofFloat(startF, endF);
valueAnimator.setDuration(time);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setRepeatCount(SetAnimRepeatCount());
if (ValueAnimator.RESTART == SetAnimRepeatMode()) {
valueAnimator.setRepeatMode(ValueAnimator.RESTART);
} else if (ValueAnimator.REVERSE == SetAnimRepeatMode()) {
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
}
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
OnAnimationUpdate(valueAnimator);
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
}
@Override
public void onAnimationRepeat(Animator animation) {
super.onAnimationRepeat(animation);
OnAnimationRepeat(animation);
}
});
if (!valueAnimator.isRunning()) {
AnimIsRunning();
valueAnimator.start();
}
return valueAnimator;
}
protected abstract void InitPaint();
protected abstract void OnAnimationUpdate(ValueAnimator valueAnimator);
protected abstract void OnAnimationRepeat(Animator animation);
protected abstract int OnStopAnim();
protected abstract int SetAnimRepeatMode();
protected abstract int SetAnimRepeatCount();
protected abstract void AnimIsRunning();
public float getFontlength(Paint paint, String str) {
Rect rect = new Rect();
paint.getTextBounds(str, 0, str.length(), rect);
return rect.width();
}
public float getFontHeight(Paint paint, String str) {
Rect rect = new Rect();
paint.getTextBounds(str, 0, str.length(), rect);
return rect.height();
}
public float getFontHeight(Paint paint) {
Paint.FontMetrics fm = paint.getFontMetrics();
return fm.descent - fm.ascent;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/CardStackAdapter.java
================================================
package io.virtualapp.widgets;
import java.util.ArrayList;
import java.util.List;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import io.virtualapp.R;
/**
* This class acts as an adapter for the {@link CardStackLayout} view. This
* adapter is intentionally made an abstract class with following abstract
* methods -
*
*
* {@link #getCount()} - Decides the number of views present in the view
*
* {@link #createView(int, ViewGroup)} - Creates the view for all positions in
* range [0, {@link #getCount()})
*
* Contains the logic for touch events in {@link #onTouch(View, MotionEvent)}
*/
public abstract class CardStackAdapter implements View.OnTouchListener, View.OnClickListener {
public static final int ANIM_DURATION = 600;
public static final int DECELERATION_FACTOR = 2;
public static final int INVALID_CARD_POSITION = -1;
private final int mScreenHeight;
private final int dp30;
// Settings for the adapter from layout
private float mCardGapBottom;
private float mCardGap;
private int mParallaxScale;
private boolean mParallaxEnabled;
private boolean mShowInitAnimation;
private int fullCardHeight;
private View[] mCardViews;
private float dp8;
private CardStackLayout mParent;
private boolean mScreenTouchable = false;
private float mTouchFirstY = -1;
private float mTouchPrevY = -1;
private float mTouchDistance = 0;
private int mSelectedCardPosition = INVALID_CARD_POSITION;
private float scaleFactorForElasticEffect;
private int mParentPaddingTop = 0;
private int mCardPaddingInternal = 0;
public CardStackAdapter(Context context) {
Resources resources = context.getResources();
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
mScreenHeight = dm.heightPixels;
dp30 = (int) resources.getDimension(R.dimen.dp30);
scaleFactorForElasticEffect = (int) resources.getDimension(R.dimen.dp8);
dp8 = (int) resources.getDimension(R.dimen.dp8);
}
protected float getCardGapBottom() {
return mCardGapBottom;
}
/**
* Defines and initializes the view to be shown in the
* {@link CardStackLayout} Provides two parameters to the sub-class namely -
*
* @param position
* @param container
* @return View corresponding to the position and parent container
*/
public abstract View createView(int position, ViewGroup container);
/**
* Defines the number of cards that are present in the
* {@link CardStackLayout}
*
* @return cardCount - Number of views in the related
* {@link CardStackLayout}
*/
public abstract int getCount();
/**
* Returns true if no animation is in progress currently. Can be used to
* disable any events if they are not allowed during an animation. Returns
* false if an animation is in progress.
*
* @return - true if animation in progress, false otherwise
*/
public boolean isScreenTouchable() {
return mScreenTouchable;
}
private void setScreenTouchable(boolean screenTouchable) {
this.mScreenTouchable = screenTouchable;
}
void addView(final int position) {
View root = createView(position, mParent);
root.setOnTouchListener(this);
root.setTag(R.id.cardstack_internal_position_tag, position);
root.setLayerType(View.LAYER_TYPE_HARDWARE, null);
mCardPaddingInternal = root.getPaddingTop();
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, fullCardHeight);
root.setLayoutParams(lp);
if (mShowInitAnimation) {
root.setY(getCardFinalY(position));
setScreenTouchable(false);
} else {
root.setY(getCardOriginalY(position) - mParentPaddingTop);
setScreenTouchable(true);
}
mCardViews[position] = root;
mParent.addView(root);
}
protected float getCardFinalY(int position) {
return mScreenHeight - dp30 - ((getCount() - position) * mCardGapBottom) - mCardPaddingInternal;
}
protected float getCardOriginalY(int position) {
return mParentPaddingTop + mCardGap * position;
}
/**
* Resets all cards in {@link CardStackLayout} to their initial positions
*
* @param r
* Execute r.run() once the reset animation is done
*/
public void resetCards(Runnable r) {
List animations = new ArrayList<>(getCount());
for (int i = 0; i < getCount(); i++) {
final View child = mCardViews[i];
animations.add(ObjectAnimator.ofFloat(child, View.Y, (int) child.getY(), getCardOriginalY(i)));
}
startAnimations(animations, r, true);
}
/**
* Plays together all animations passed in as parameter. Once animation is
* completed, r.run() is executed. If parameter isReset is set to true,
* {@link #mSelectedCardPosition} is set to {@link #INVALID_CARD_POSITION}
*
* @param animations
* @param r
* @param isReset
*/
private void startAnimations(List animations, final Runnable r, final boolean isReset) {
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animations);
animatorSet.setDuration(ANIM_DURATION);
animatorSet.setInterpolator(new DecelerateInterpolator(DECELERATION_FACTOR));
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (r != null)
r.run();
setScreenTouchable(true);
if (isReset)
mSelectedCardPosition = INVALID_CARD_POSITION;
}
});
animatorSet.start();
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (!isScreenTouchable()) {
return false;
}
float y = event.getRawY();
int positionOfCardToMove = (int) v.getTag(R.id.cardstack_internal_position_tag);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
if (mTouchFirstY != -1) {
return false;
}
mTouchPrevY = mTouchFirstY = y;
mTouchDistance = 0;
break;
case MotionEvent.ACTION_MOVE :
if (mSelectedCardPosition == INVALID_CARD_POSITION)
moveCards(positionOfCardToMove, y - mTouchFirstY);
mTouchDistance += Math.abs(y - mTouchPrevY);
break;
case MotionEvent.ACTION_CANCEL :
case MotionEvent.ACTION_UP :
if (mTouchDistance < dp8 && Math.abs(y - mTouchFirstY) < dp8
&& mSelectedCardPosition == INVALID_CARD_POSITION) {
onClick(v);
} else {
resetCards();
}
mTouchPrevY = mTouchFirstY = -1;
mTouchDistance = 0;
return false;
}
return true;
}
@Override
public void onClick(final View v) {
if (!isScreenTouchable()) {
return;
}
setScreenTouchable(false);
if (mSelectedCardPosition == INVALID_CARD_POSITION) {
mSelectedCardPosition = (int) v.getTag(R.id.cardstack_internal_position_tag);
List animations = new ArrayList<>(getCount());
for (int i = 0; i < getCount(); i++) {
View child = mCardViews[i];
animations.add(getAnimatorForView(child, i, mSelectedCardPosition));
}
startAnimations(animations, () -> {
setScreenTouchable(true);
if (mParent.getOnCardSelectedListener() != null) {
mParent.getOnCardSelectedListener().onCardSelected(v, mSelectedCardPosition);
}
}, false);
}
}
/**
* This method can be overridden to have different animations for each card
* when a click event happens on any card view. This method will be called
* for every
*
* @param view
* The view for which this method needs to return an animator
* @param selectedCardPosition
* Position of the card that was clicked
* @param currentCardPosition
* Position of the current card
* @return animator which has to be applied on the current card
*/
protected Animator getAnimatorForView(View view, int currentCardPosition, int selectedCardPosition) {
if (currentCardPosition != selectedCardPosition) {
return ObjectAnimator.ofFloat(view, View.Y, (int) view.getY(), getCardFinalY(currentCardPosition));
} else {
return ObjectAnimator.ofFloat(view, View.Y, (int) view.getY(),
getCardOriginalY(0) + (currentCardPosition * mCardGapBottom));
}
}
private void moveCards(int positionOfCardToMove, float diff) {
if (diff < 0 || positionOfCardToMove < 0 || positionOfCardToMove >= getCount())
return;
for (int i = positionOfCardToMove; i < getCount(); i++) {
final View child = mCardViews[i];
float diffCard = diff / scaleFactorForElasticEffect;
if (mParallaxEnabled) {
if (mParallaxScale > 0) {
diffCard = diffCard * (mParallaxScale / 3) * (getCount() + 1 - i);
} else {
int scale = mParallaxScale * -1;
diffCard = diffCard * (i * (scale / 3) + 1);
}
} else
diffCard = diffCard * (getCount() * 2 + 1);
child.setY(getCardOriginalY(i) + diffCard);
}
}
/**
* Provides an API to {@link CardStackLayout} to set the parameters provided
* to it in its XML
*
* @param cardStackLayout
* Parent of all cards
*/
void setAdapterParams(CardStackLayout cardStackLayout) {
mParent = cardStackLayout;
mCardViews = new View[getCount()];
mCardGapBottom = cardStackLayout.getCardGapBottom();
mCardGap = cardStackLayout.getCardGap();
mParallaxScale = cardStackLayout.getParallaxScale();
mParallaxEnabled = cardStackLayout.isParallaxEnabled();
if (mParallaxEnabled && mParallaxScale == 0)
mParallaxEnabled = false;
mShowInitAnimation = cardStackLayout.isShowInitAnimation();
mParentPaddingTop = cardStackLayout.getPaddingTop();
fullCardHeight = (int) (mScreenHeight - dp30 - dp8 - getCount() * mCardGapBottom);
}
/**
* Resets all cards in {@link CardStackLayout} to their initial positions
*/
public void resetCards() {
resetCards(null);
}
/**
* Returns false if all the cards are in their initial position i.e. no card
* is selected
*
* Returns true if the {@link CardStackLayout} has a card selected and all
* other cards are at the bottom of the screen.
*
* @return true if any card is selected, false otherwise
*/
public boolean isCardSelected() {
return mSelectedCardPosition != INVALID_CARD_POSITION;
}
/**
* Returns the position of selected card. If no card is selected, returns
* {@link #INVALID_CARD_POSITION}
*/
public int getSelectedCardPosition() {
return mSelectedCardPosition;
}
/**
* Since there is no view recycling in {@link CardStackLayout}, we maintain
* an instance of every view that is set for every position. This method
* returns a view at the requested position.
*
* @param position
* Position of card in {@link CardStackLayout}
* @return View at requested position
*/
public View getCardView(int position) {
if (mCardViews == null)
return null;
return mCardViews[position];
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/CardStackLayout.java
================================================
package io.virtualapp.widgets;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import io.virtualapp.R;
/**
* Displays a list of cards as a stack on the screen.
*
* XML attributes
*
* See {@link R.styleable#CardStackLayout CardStackLayout Attributes}
*
* {@link R.styleable#CardStackLayout_showInitAnimation}
* {@link R.styleable#CardStackLayout_card_gap}
* {@link R.styleable#CardStackLayout_card_gap_bottom}
* {@link R.styleable#CardStackLayout_parallax_enabled}
* {@link R.styleable#CardStackLayout_parallax_scale}
*/
public class CardStackLayout extends FrameLayout {
public static final boolean PARALLAX_ENABLED_DEFAULT = false;
public static final boolean SHOW_INIT_ANIMATION_DEFAULT = true;
private float mCardGapBottom;
private float mCardGap;
private boolean mShowInitAnimation;
private boolean mParallaxEnabled;
private int mParallaxScale;
private OnCardSelected mOnCardSelectedListener = null;
private CardStackAdapter mAdapter = null;
public CardStackLayout(Context context) {
super(context);
resetDefaults();
}
public CardStackLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CardStackLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
handleArgs(context, attrs, defStyleAttr, 0);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public CardStackLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
handleArgs(context, attrs, defStyleAttr, defStyleRes);
}
/**
* package restricted
*/
OnCardSelected getOnCardSelectedListener() {
return mOnCardSelectedListener;
}
/**
* Listen on card selection events for {@link CardStackLayout}. Sends
* clicked view and it's corresponding position in the callback.
*
* @param onCardSelectedListener
* listener
*/
public void setOnCardSelectedListener(OnCardSelected onCardSelectedListener) {
this.mOnCardSelectedListener = onCardSelectedListener;
}
private void resetDefaults() {
mOnCardSelectedListener = null;
}
private void handleArgs(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
resetDefaults();
final TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CardStackLayout, defStyleAttr,
defStyleRes);
mParallaxEnabled = a.getBoolean(R.styleable.CardStackLayout_parallax_enabled, PARALLAX_ENABLED_DEFAULT);
mShowInitAnimation = a.getBoolean(R.styleable.CardStackLayout_showInitAnimation, SHOW_INIT_ANIMATION_DEFAULT);
mParallaxScale = a.getInteger(R.styleable.CardStackLayout_parallax_scale,
getResources().getInteger(R.integer.parallax_scale_default));
mCardGap = a.getDimension(R.styleable.CardStackLayout_card_gap, getResources().getDimension(R.dimen.card_gap));
mCardGapBottom = a.getDimension(R.styleable.CardStackLayout_card_gap_bottom,
getResources().getDimension(R.dimen.card_gap_bottom));
a.recycle();
}
/**
* @return adapter of type {@link CardStackAdapter} that is set for this
* view.
*/
public CardStackAdapter getAdapter() {
return mAdapter;
}
/**
* Set the adapter for this {@link CardStackLayout}
*
* @param adapter
* Should extend {@link CardStackAdapter}
*/
public void setAdapter(CardStackAdapter adapter) {
this.mAdapter = adapter;
mAdapter.setAdapterParams(this);
for (int i = 0; i < mAdapter.getCount(); i++) {
mAdapter.addView(i);
}
if (mShowInitAnimation) {
postDelayed(this::restoreCards, 500);
}
}
/**
* @return currently set parallax scale value.
*/
public int getParallaxScale() {
return mParallaxScale;
}
/**
* Sets the value of parallax scale. Parallax scale is the factor which
* decides how much distance a card will scroll when the user drags it down.
*/
public void setParallaxScale(int mParallaxScale) {
this.mParallaxScale = mParallaxScale;
}
public boolean isParallaxEnabled() {
return mParallaxEnabled;
}
public void setParallaxEnabled(boolean mParallaxEnabled) {
this.mParallaxEnabled = mParallaxEnabled;
}
public boolean isShowInitAnimation() {
return mShowInitAnimation;
}
public void setShowInitAnimation(boolean mShowInitAnimation) {
this.mShowInitAnimation = mShowInitAnimation;
}
/**
* @return the gap (in pixels) between two consecutive cards
*/
public float getCardGap() {
return mCardGap;
}
/**
* Set the gap (in pixels) between two consecutive cards
*/
public void setCardGap(float mCardGap) {
this.mCardGap = mCardGap;
}
/**
* @return gap between the two consecutive cards when collapsed to the
* bottom of the screen
*/
public float getCardGapBottom() {
return mCardGapBottom;
}
public void setCardGapBottom(float mCardGapBottom) {
this.mCardGapBottom = mCardGapBottom;
}
/**
* @return true if a card is selected, false otherwise
*/
public boolean isCardSelected() {
return mAdapter.isCardSelected();
}
/**
* Removes the adapter that was previously set using
* {@link #setAdapter(CardStackAdapter)}
*/
public void removeAdapter() {
if (getChildCount() > 0)
removeAllViews();
mAdapter = null;
mOnCardSelectedListener = null;
}
/**
* Animates the cards to their initial position in the layout.
*/
public void restoreCards() {
mAdapter.resetCards();
}
/**
* Intimates the implementing class about the selection of a card
*/
public interface OnCardSelected {
void onCardSelected(View v, int position);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/CircularAnim.java
================================================
package io.virtualapp.widgets;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.widget.ImageView;
public class CircularAnim {
public static final long PERFECT_MILLS = 618;
public static final int MINI_RADIUS = 0;
private static Long sPerfectMills;
private static Long sFullActivityPerfectMills;
private static Integer sColorOrImageRes;
private static long getPerfectMills() {
if (sPerfectMills != null)
return sPerfectMills;
else
return PERFECT_MILLS;
}
private static long getFullActivityMills() {
if (sFullActivityPerfectMills != null)
return sFullActivityPerfectMills;
else
return PERFECT_MILLS;
}
private static int getColorOrImageRes() {
if (sColorOrImageRes != null)
return sColorOrImageRes;
else
return android.R.color.white;
}
public static VisibleBuilder show(View animView) {
return new VisibleBuilder(animView, true);
}
public static VisibleBuilder hide(View animView) {
return new VisibleBuilder(animView, false);
}
public static FullActivityBuilder fullActivity(Activity activity, View triggerView) {
return new FullActivityBuilder(activity, triggerView);
}
public static void init(long perfectMills, long fullActivityPerfectMills, int colorOrImageRes) {
sPerfectMills = perfectMills;
sFullActivityPerfectMills = fullActivityPerfectMills;
sColorOrImageRes = colorOrImageRes;
}
public interface OnAnimationEndListener {
void onAnimationEnd();
}
@SuppressLint("NewApi")
public static class VisibleBuilder {
private View mAnimView, mTriggerView;
private Float mStartRadius, mEndRadius;
private long mDurationMills = getPerfectMills();
private boolean isShow;
private OnAnimationEndListener mOnAnimationEndListener;
public VisibleBuilder(View animView, boolean isShow) {
mAnimView = animView;
this.isShow = isShow;
if (isShow) {
mStartRadius = MINI_RADIUS + 0F;
} else {
mEndRadius = MINI_RADIUS + 0F;
}
}
public VisibleBuilder triggerView(View triggerView) {
mTriggerView = triggerView;
return this;
}
public VisibleBuilder startRadius(float startRadius) {
mStartRadius = startRadius;
return this;
}
public VisibleBuilder endRadius(float endRadius) {
mEndRadius = endRadius;
return this;
}
public VisibleBuilder duration(long durationMills) {
mDurationMills = durationMills;
return this;
}
@Deprecated //You can use method - go(OnAnimationEndListener onAnimationEndListener).
public VisibleBuilder onAnimationEndListener(OnAnimationEndListener onAnimationEndListener) {
mOnAnimationEndListener = onAnimationEndListener;
return this;
}
public void go() {
go(null);
}
public void go(OnAnimationEndListener onAnimationEndListener) {
mOnAnimationEndListener = onAnimationEndListener;
// 版本判断
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
doOnEnd();
return;
}
int rippleCX, rippleCY, maxRadius;
if (mTriggerView != null) {
int[] tvLocation = new int[2];
mTriggerView.getLocationInWindow(tvLocation);
final int tvCX = tvLocation[0] + mTriggerView.getWidth() / 2;
final int tvCY = tvLocation[1] + mTriggerView.getHeight() / 2;
int[] avLocation = new int[2];
mAnimView.getLocationInWindow(avLocation);
final int avLX = avLocation[0];
final int avTY = avLocation[1];
int triggerX = Math.max(avLX, tvCX);
triggerX = Math.min(triggerX, avLX + mAnimView.getWidth());
int triggerY = Math.max(avTY, tvCY);
triggerY = Math.min(triggerY, avTY + mAnimView.getHeight());
// 以上全为绝对坐标
int avW = mAnimView.getWidth();
int avH = mAnimView.getHeight();
rippleCX = triggerX - avLX;
rippleCY = triggerY - avTY;
// 计算水波中心点至 @mAnimView 边界的最大距离
int maxW = Math.max(rippleCX, avW - rippleCX);
int maxH = Math.max(rippleCY, avH - rippleCY);
maxRadius = (int) Math.sqrt(maxW * maxW + maxH * maxH) + 1;
} else {
rippleCX = (mAnimView.getLeft() + mAnimView.getRight()) / 2;
rippleCY = (mAnimView.getTop() + mAnimView.getBottom()) / 2;
int w = mAnimView.getWidth();
int h = mAnimView.getHeight();
// 勾股定理 & 进一法
maxRadius = (int) Math.sqrt(w * w + h * h) + 1;
}
if (isShow && mEndRadius == null)
mEndRadius = maxRadius + 0F;
else if (!isShow && mStartRadius == null)
mStartRadius = maxRadius + 0F;
try {
Animator anim = ViewAnimationUtils.createCircularReveal(
mAnimView, rippleCX, rippleCY, mStartRadius, mEndRadius);
mAnimView.setVisibility(View.VISIBLE);
anim.setDuration(mDurationMills);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
doOnEnd();
}
});
anim.start();
} catch (Exception e) {
e.printStackTrace();
doOnEnd();
}
}
private void doOnEnd() {
if (isShow)
mAnimView.setVisibility(View.VISIBLE);
else
mAnimView.setVisibility(View.INVISIBLE);
if (mOnAnimationEndListener != null)
mOnAnimationEndListener.onAnimationEnd();
}
}
@SuppressLint("NewApi")
public static class FullActivityBuilder {
private Activity mActivity;
private View mTriggerView;
private float mStartRadius = MINI_RADIUS;
private int mColorOrImageRes = getColorOrImageRes();
private Long mDurationMills;
private OnAnimationEndListener mOnAnimationEndListener;
private int mEnterAnim = android.R.anim.fade_in, mExitAnim = android.R.anim.fade_out;
public FullActivityBuilder(Activity activity, View triggerView) {
mActivity = activity;
mTriggerView = triggerView;
}
public FullActivityBuilder startRadius(float startRadius) {
mStartRadius = startRadius;
return this;
}
public FullActivityBuilder colorOrImageRes(int colorOrImageRes) {
mColorOrImageRes = colorOrImageRes;
return this;
}
public FullActivityBuilder duration(long durationMills) {
mDurationMills = durationMills;
return this;
}
public FullActivityBuilder overridePendingTransition(int enterAnim, int exitAnim) {
mEnterAnim = enterAnim;
mExitAnim = exitAnim;
return this;
}
public void go(OnAnimationEndListener onAnimationEndListener) {
mOnAnimationEndListener = onAnimationEndListener;
// 版本判断,小于5.0则无动画.
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
doOnEnd();
return;
}
int[] location = new int[2];
mTriggerView.getLocationInWindow(location);
final int cx = location[0] + mTriggerView.getWidth() / 2;
final int cy = location[1] + mTriggerView.getHeight() / 2;
final ImageView view = new ImageView(mActivity);
view.setScaleType(ImageView.ScaleType.CENTER_CROP);
view.setImageResource(mColorOrImageRes);
final ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView();
int w = decorView.getWidth();
int h = decorView.getHeight();
decorView.addView(view, w, h);
int maxW = Math.max(cx, w - cx);
int maxH = Math.max(cy, h - cy);
final int finalRadius = (int) Math.sqrt(maxW * maxW + maxH * maxH) + 1;
try {
Animator anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, mStartRadius, finalRadius);
int maxRadius = (int) Math.sqrt(w * w + h * h) + 1;
if (mDurationMills == null) {
double rate = 1d * finalRadius / maxRadius;
mDurationMills = (long) (getFullActivityMills() * Math.sqrt(rate));
}
final long finalDuration = mDurationMills;
anim.setDuration((long) (finalDuration * 0.9));
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
doOnEnd();
mActivity.overridePendingTransition(mEnterAnim, mExitAnim);
mTriggerView.postDelayed(new Runnable() {
@Override
public void run() {
if (mActivity.isFinishing()) return;
try {
Animator anim = ViewAnimationUtils.createCircularReveal(view, cx, cy,
finalRadius, mStartRadius);
anim.setDuration(finalDuration);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
try {
decorView.removeView(view);
} catch (Exception e) {
e.printStackTrace();
}
}
});
anim.start();
} catch (Exception e) {
e.printStackTrace();
try {
decorView.removeView(view);
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
}, 1000);
}
});
anim.start();
} catch (Exception e) {
e.printStackTrace();
doOnEnd();
}
}
private void doOnEnd() {
mOnAnimationEndListener.onAnimationEnd();
}
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/DragSelectRecyclerView.java
================================================
package io.virtualapp.widgets;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import io.virtualapp.R;
/**
* @author Aidan Follestad (afollestad)
*/
public class DragSelectRecyclerView extends RecyclerView {
private static final boolean LOGGING = false;
private static final int AUTO_SCROLL_DELAY = 25;
private int mLastDraggedIndex = -1;
private DragSelectRecyclerViewAdapter> mAdapter;
private int mInitialSelection;
private boolean mDragSelectActive;
private int mMinReached;
private int mMaxReached;
private int mHotspotHeight;
private int mHotspotOffsetTop;
private int mHotspotOffsetBottom;
private int mHotspotTopBoundStart;
private int mHotspotTopBoundEnd;
private int mHotspotBottomBoundStart;
private int mHotspotBottomBoundEnd;
private int mAutoScrollVelocity;
private FingerListener mFingerListener;
private boolean mInTopHotspot;
private boolean mInBottomHotspot;
private Handler mAutoScrollHandler;
private Runnable mAutoScrollRunnable = new Runnable() {
@Override
public void run() {
if (mAutoScrollHandler == null)
return;
if (mInTopHotspot) {
scrollBy(0, -mAutoScrollVelocity);
mAutoScrollHandler.postDelayed(this, AUTO_SCROLL_DELAY);
} else if (mInBottomHotspot) {
scrollBy(0, mAutoScrollVelocity);
mAutoScrollHandler.postDelayed(this, AUTO_SCROLL_DELAY);
}
}
};
private RectF mTopBoundRect;
private RectF mBottomBoundRect;
private Paint mDebugPaint;
private boolean mDebugEnabled = false;
public DragSelectRecyclerView(Context context) {
super(context);
init(context, null);
}
public DragSelectRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public DragSelectRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private static void LOG(String message, Object... args) {
//noinspection PointlessBooleanExpression
if (!LOGGING) return;
if (args != null) {
Log.d("DragSelectRecyclerView", String.format(message, args));
} else {
Log.d("DragSelectRecyclerView", message);
}
}
private void init(Context context, AttributeSet attrs) {
mAutoScrollHandler = new Handler();
final int defaultHotspotHeight = context.getResources().getDimensionPixelSize(R.dimen.dsrv_defaultHotspotHeight);
if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DragSelectRecyclerView, 0, 0);
try {
boolean autoScrollEnabled = a.getBoolean(R.styleable.DragSelectRecyclerView_dsrv_autoScrollEnabled, true);
if (!autoScrollEnabled) {
mHotspotHeight = -1;
mHotspotOffsetTop = -1;
mHotspotOffsetBottom = -1;
LOG("Auto-scroll disabled");
} else {
mHotspotHeight = a.getDimensionPixelSize(
R.styleable.DragSelectRecyclerView_dsrv_autoScrollHotspotHeight, defaultHotspotHeight);
mHotspotOffsetTop = a.getDimensionPixelSize(
R.styleable.DragSelectRecyclerView_dsrv_autoScrollHotspot_offsetTop, 0);
mHotspotOffsetBottom = a.getDimensionPixelSize(
R.styleable.DragSelectRecyclerView_dsrv_autoScrollHotspot_offsetBottom, 0);
LOG("Hotspot height = %d", mHotspotHeight);
}
} finally {
a.recycle();
}
} else {
mHotspotHeight = defaultHotspotHeight;
LOG("Hotspot height = %d", mHotspotHeight);
}
}
public void setFingerListener(@Nullable FingerListener listener) {
this.mFingerListener = listener;
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
super.onMeasure(widthSpec, heightSpec);
if (mHotspotHeight > -1) {
mHotspotTopBoundStart = mHotspotOffsetTop;
mHotspotTopBoundEnd = mHotspotOffsetTop + mHotspotHeight;
mHotspotBottomBoundStart = (getMeasuredHeight() - mHotspotHeight) - mHotspotOffsetBottom;
mHotspotBottomBoundEnd = getMeasuredHeight() - mHotspotOffsetBottom;
LOG("RecyclerView height = %d", getMeasuredHeight());
LOG("Hotspot top bound = %d to %d", mHotspotTopBoundStart, mHotspotTopBoundStart);
LOG("Hotspot bottom bound = %d to %d", mHotspotBottomBoundStart, mHotspotBottomBoundEnd);
}
}
public boolean setDragSelectActive(boolean active, int initialSelection) {
if (active && mDragSelectActive) {
LOG("Drag selection is already active.");
return false;
}
mLastDraggedIndex = -1;
mMinReached = -1;
mMaxReached = -1;
if (!mAdapter.isIndexSelectable(initialSelection)) {
mDragSelectActive = false;
mInitialSelection = -1;
mLastDraggedIndex = -1;
LOG("Index %d is not selectable.", initialSelection);
return false;
}
mAdapter.setSelected(initialSelection, true);
mDragSelectActive = active;
mInitialSelection = initialSelection;
mLastDraggedIndex = initialSelection;
if (mFingerListener != null)
mFingerListener.onDragSelectFingerAction(true);
LOG("Drag selection initialized, starting at index %d.", initialSelection);
return true;
}
/**
* Use {@link #setAdapter(DragSelectRecyclerViewAdapter)} instead.
*/
@Override
@Deprecated
public void setAdapter(Adapter adapter) {
if (!(adapter instanceof DragSelectRecyclerViewAdapter>))
throw new IllegalArgumentException("Adapter must be a DragSelectRecyclerViewAdapter.");
setAdapter((DragSelectRecyclerViewAdapter>) adapter);
}
public void setAdapter(DragSelectRecyclerViewAdapter> adapter) {
super.setAdapter(adapter);
mAdapter = adapter;
}
private int getItemPosition(MotionEvent e) {
final View v = findChildViewUnder(e.getX(), e.getY());
if (v == null) return NO_POSITION;
if (v.getTag() == null || !(v.getTag() instanceof ViewHolder))
throw new IllegalStateException("Make sure your adapter makes a call to super.onBindViewHolder(), and doesn't override itemView tags.");
final ViewHolder holder = (ViewHolder) v.getTag();
return holder.getAdapterPosition();
}
public final void enableDebug() {
mDebugEnabled = true;
invalidate();
}
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
if (mDebugEnabled) {
if (mDebugPaint == null) {
mDebugPaint = new Paint();
mDebugPaint.setColor(Color.BLACK);
mDebugPaint.setAntiAlias(true);
mDebugPaint.setStyle(Paint.Style.FILL);
mTopBoundRect = new RectF(0, mHotspotTopBoundStart, getMeasuredWidth(), mHotspotTopBoundEnd);
mBottomBoundRect = new RectF(0, mHotspotBottomBoundStart, getMeasuredWidth(), mHotspotBottomBoundEnd);
}
c.drawRect(mTopBoundRect, mDebugPaint);
c.drawRect(mBottomBoundRect, mDebugPaint);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent e) {
if (mAdapter.getItemCount() == 0)
return super.dispatchTouchEvent(e);
if (mDragSelectActive) {
if (e.getAction() == MotionEvent.ACTION_UP) {
mDragSelectActive = false;
mInTopHotspot = false;
mInBottomHotspot = false;
mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable);
if (mFingerListener != null)
mFingerListener.onDragSelectFingerAction(false);
return true;
} else if (e.getAction() == MotionEvent.ACTION_MOVE) {
// Check for auto-scroll hotspot
if (mHotspotHeight > -1) {
if (e.getY() >= mHotspotTopBoundStart && e.getY() <= mHotspotTopBoundEnd) {
mInBottomHotspot = false;
if (!mInTopHotspot) {
mInTopHotspot = true;
LOG("Now in TOP hotspot");
mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable);
mAutoScrollHandler.postDelayed(mAutoScrollRunnable, AUTO_SCROLL_DELAY);
}
final float simulatedFactor = mHotspotTopBoundEnd - mHotspotTopBoundStart;
final float simulatedY = e.getY() - mHotspotTopBoundStart;
mAutoScrollVelocity = (int) (simulatedFactor - simulatedY) / 2;
LOG("Auto scroll velocity = %d", mAutoScrollVelocity);
} else if (e.getY() >= mHotspotBottomBoundStart && e.getY() <= mHotspotBottomBoundEnd) {
mInTopHotspot = false;
if (!mInBottomHotspot) {
mInBottomHotspot = true;
LOG("Now in BOTTOM hotspot");
mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable);
mAutoScrollHandler.postDelayed(mAutoScrollRunnable, AUTO_SCROLL_DELAY);
}
final float simulatedY = e.getY() + mHotspotBottomBoundEnd;
final float simulatedFactor = mHotspotBottomBoundStart + mHotspotBottomBoundEnd;
mAutoScrollVelocity = (int) (simulatedY - simulatedFactor) / 2;
LOG("Auto scroll velocity = %d", mAutoScrollVelocity);
} else if (mInTopHotspot || mInBottomHotspot) {
LOG("Left the hotspot");
mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable);
mInTopHotspot = false;
mInBottomHotspot = false;
}
}
// Drag selection logic
// NOTE: DISABLE IT
// if (itemPosition != NO_POSITION && mLastDraggedIndex != itemPosition) {
// mLastDraggedIndex = itemPosition;
// if (mMinReached == -1) mMinReached = mLastDraggedIndex;
// if (mMaxReached == -1) mMaxReached = mLastDraggedIndex;
// if (mLastDraggedIndex > mMaxReached)
// mMaxReached = mLastDraggedIndex;
// if (mLastDraggedIndex < mMinReached)
// mMinReached = mLastDraggedIndex;
// if (mAdapter != null)
// mAdapter.selectRange(mInitialSelection, mLastDraggedIndex, mMinReached, mMaxReached);
// if (mInitialSelection == mLastDraggedIndex) {
// mMinReached = mLastDraggedIndex;
// mMaxReached = mLastDraggedIndex;
// }
// }
return true;
}
}
return super.dispatchTouchEvent(e);
}
public interface FingerListener {
void onDragSelectFingerAction(boolean fingerDown);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/DragSelectRecyclerViewAdapter.java
================================================
package io.virtualapp.widgets;
import android.os.Bundle;
import android.support.annotation.CallSuper;
import android.support.v7.widget.RecyclerView;
import java.util.ArrayList;
/**
* @author Aidan Follestad (afollestad)
*/
public abstract class DragSelectRecyclerViewAdapter extends RecyclerView.Adapter {
private ArrayList mSelectedIndices;
private SelectionListener mSelectionListener;
private int mLastCount = -1;
private int mMaxSelectionCount = -1;
protected DragSelectRecyclerViewAdapter() {
mSelectedIndices = new ArrayList<>();
}
private void fireSelectionListener() {
if (mLastCount == mSelectedIndices.size())
return;
mLastCount = mSelectedIndices.size();
if (mSelectionListener != null)
mSelectionListener.onDragSelectionChanged(mLastCount);
}
public void setMaxSelectionCount(int maxSelectionCount) {
this.mMaxSelectionCount = maxSelectionCount;
}
public void setSelectionListener(SelectionListener selectionListener) {
this.mSelectionListener = selectionListener;
}
public void saveInstanceState(Bundle out) {
saveInstanceState("selected_indices", out);
}
public void saveInstanceState(String key, Bundle out) {
out.putSerializable(key, mSelectedIndices);
}
public void restoreInstanceState(Bundle in) {
restoreInstanceState("selected_indices", in);
}
public void restoreInstanceState(String key, Bundle in) {
if (in != null && in.containsKey(key)) {
//noinspection unchecked
mSelectedIndices = (ArrayList) in.getSerializable(key);
if (mSelectedIndices == null) mSelectedIndices = new ArrayList<>();
else fireSelectionListener();
}
}
public final void setSelected(int index, boolean selected) {
if (!isIndexSelectable(index))
selected = false;
if (selected) {
if (!mSelectedIndices.contains(index) &&
(mMaxSelectionCount == -1 ||
mSelectedIndices.size() < mMaxSelectionCount)) {
mSelectedIndices.add(index);
notifyItemChanged(index);
}
} else if (mSelectedIndices.contains(index)) {
mSelectedIndices.remove((Integer) index);
notifyItemChanged(index);
}
fireSelectionListener();
}
public final boolean toggleSelected(int index) {
boolean selectedNow = false;
if (isIndexSelectable(index)) {
if (mSelectedIndices.contains(index)) {
mSelectedIndices.remove((Integer) index);
} else if (mMaxSelectionCount == -1 ||
mSelectedIndices.size() < mMaxSelectionCount) {
mSelectedIndices.add(index);
selectedNow = true;
}
notifyItemChanged(index);
}
fireSelectionListener();
return selectedNow;
}
protected boolean isIndexSelectable(int index) {
return true;
}
@CallSuper
@Override
public void onBindViewHolder(VH holder, int position) {
holder.itemView.setTag(holder);
}
public final void selectRange(int from, int to, int min, int max) {
if (from == to) {
// Finger is back on the initial item, unselect everything else
for (int i = min; i <= max; i++) {
if (i == from) continue;
setSelected(i, false);
}
fireSelectionListener();
return;
}
if (to < from) {
// When selecting from one to previous items
for (int i = to; i <= from; i++)
setSelected(i, true);
if (min > -1 && min < to) {
// Unselect items that were selected during this drag but no longer are
for (int i = min; i < to; i++) {
if (i == from) continue;
setSelected(i, false);
}
}
if (max > -1) {
for (int i = from + 1; i <= max; i++)
setSelected(i, false);
}
} else {
// When selecting from one to next items
for (int i = from; i <= to; i++)
setSelected(i, true);
if (max > -1 && max > to) {
// Unselect items that were selected during this drag but no longer are
for (int i = to + 1; i <= max; i++) {
if (i == from) continue;
setSelected(i, false);
}
}
if (min > -1) {
for (int i = min; i < from; i++)
setSelected(i, false);
}
}
fireSelectionListener();
}
public final void selectAll() {
int max = getItemCount();
mSelectedIndices.clear();
for (int i = 0; i < max; i++) {
if (isIndexSelectable(i)) {
mSelectedIndices.add(i);
}
}
notifyDataSetChanged();
fireSelectionListener();
}
public final void clearSelected() {
mSelectedIndices.clear();
notifyDataSetChanged();
fireSelectionListener();
}
public final int getSelectedCount() {
return mSelectedIndices.size();
}
public final Integer[] getSelectedIndices() {
return mSelectedIndices.toArray(new Integer[mSelectedIndices.size()]);
}
public final boolean isIndexSelected(int index) {
return mSelectedIndices.contains(index);
}
public interface SelectionListener {
void onDragSelectionChanged(int count);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/EatBeansView.java
================================================
package io.virtualapp.widgets;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
public class EatBeansView extends BaseView {
int eatSpeed = 5;
private Paint mPaint, mPaintEye;
private float mWidth = 0f;
private float mHigh = 0f;
private float mPadding = 5f;
private float eatErWidth = 60f;
private float eatErPositionX = 0f;
private float beansWidth = 10f;
private float mAngle = 34;
private float eatErStartAngle = mAngle;
private float eatErEndAngle = 360 - 2 * eatErStartAngle;
public EatBeansView(Context context) {
super(context);
}
public EatBeansView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public EatBeansView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth = getMeasuredWidth();
mHigh = getMeasuredHeight();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float eatRightX = mPadding + eatErWidth + eatErPositionX;
RectF rectF = new RectF(mPadding + eatErPositionX, mHigh / 2 - eatErWidth / 2, eatRightX, mHigh / 2 + eatErWidth / 2);
canvas.drawArc(rectF, eatErStartAngle, eatErEndAngle
, true, mPaint);
canvas.drawCircle(mPadding + eatErPositionX + eatErWidth / 2,
mHigh / 2 - eatErWidth / 4,
beansWidth / 2, mPaintEye);
int beansCount = (int) ((mWidth - mPadding * 2 - eatErWidth) / beansWidth / 2);
for (int i = 0; i < beansCount; i++) {
float x = beansCount * i + beansWidth / 2 + mPadding + eatErWidth;
if (x > eatRightX) {
canvas.drawCircle(x,
mHigh / 2, beansWidth / 2, mPaint);
}
}
}
private void initPaint() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.WHITE);
mPaintEye = new Paint();
mPaintEye.setAntiAlias(true);
mPaintEye.setStyle(Paint.Style.FILL);
mPaintEye.setColor(Color.BLACK);
}
public void setViewColor(int color) {
mPaint.setColor(color);
postInvalidate();
}
public void setEyeColor(int color) {
mPaintEye.setColor(color);
postInvalidate();
}
@Override
protected void InitPaint() {
initPaint();
}
@Override
protected void OnAnimationUpdate(ValueAnimator valueAnimator) {
float mAnimatedValue = (float) valueAnimator.getAnimatedValue();
eatErPositionX = (mWidth - 2 * mPadding - eatErWidth) * mAnimatedValue;
eatErStartAngle = mAngle * (1 - (mAnimatedValue * eatSpeed - (int) (mAnimatedValue * eatSpeed)));
eatErEndAngle = 360 - eatErStartAngle * 2;
invalidate();
}
@Override
protected void OnAnimationRepeat(Animator animation) {
}
@Override
protected int OnStopAnim() {
eatErPositionX = 0;
postInvalidate();
return 1;
}
@Override
protected int SetAnimRepeatMode() {
return ValueAnimator.RESTART;
}
@Override
protected void AnimIsRunning() {
}
@Override
protected int SetAnimRepeatCount() {
return ValueAnimator.INFINITE;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/Indicator.java
================================================
package io.virtualapp.widgets;
import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import java.util.ArrayList;
import java.util.HashMap;
public abstract class Indicator extends Drawable implements Animatable {
private static final Rect ZERO_BOUNDS_RECT = new Rect();
protected Rect drawBounds = ZERO_BOUNDS_RECT;
private HashMap mUpdateListeners = new HashMap<>();
private ArrayList mAnimators;
private int alpha = 255;
private boolean mHasAnimators;
private Paint mPaint = new Paint();
public Indicator() {
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
}
public int getColor() {
return mPaint.getColor();
}
public void setColor(int color) {
mPaint.setColor(color);
}
@Override
public int getAlpha() {
return alpha;
}
@Override
public void setAlpha(int alpha) {
this.alpha = alpha;
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
}
@Override
public void draw(Canvas canvas) {
draw(canvas, mPaint);
}
public abstract void draw(Canvas canvas, Paint paint);
public abstract ArrayList onCreateAnimators();
@Override
public void start() {
ensureAnimators();
if (mAnimators == null) {
return;
}
// If the animators has not ended, do nothing.
if (isStarted()) {
return;
}
startAnimators();
invalidateSelf();
}
private void startAnimators() {
for (int i = 0; i < mAnimators.size(); i++) {
ValueAnimator animator = mAnimators.get(i);
//when the animator restart , add the updateListener again because they
// was removed by animator stop .
ValueAnimator.AnimatorUpdateListener updateListener = mUpdateListeners.get(animator);
if (updateListener != null) {
animator.addUpdateListener(updateListener);
}
animator.start();
}
}
private void stopAnimators() {
if (mAnimators != null) {
for (ValueAnimator animator : mAnimators) {
if (animator != null && animator.isStarted()) {
animator.removeAllUpdateListeners();
animator.end();
}
}
}
}
private void ensureAnimators() {
if (!mHasAnimators) {
mAnimators = onCreateAnimators();
mHasAnimators = true;
}
}
@Override
public void stop() {
stopAnimators();
}
private boolean isStarted() {
for (ValueAnimator animator : mAnimators) {
return animator.isStarted();
}
return false;
}
@Override
public boolean isRunning() {
for (ValueAnimator animator : mAnimators) {
return animator.isRunning();
}
return false;
}
/**
* Your should use this to add AnimatorUpdateListener when
* create animator , otherwise , animator doesn't work when
* the animation restart .
*
* @param updateListener
*/
public void addUpdateListener(ValueAnimator animator, ValueAnimator.AnimatorUpdateListener updateListener) {
mUpdateListeners.put(animator, updateListener);
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
setDrawBounds(bounds);
}
public void setDrawBounds(int left, int top, int right, int bottom) {
this.drawBounds = new Rect(left, top, right, bottom);
}
public void postInvalidate() {
invalidateSelf();
}
public Rect getDrawBounds() {
return drawBounds;
}
public void setDrawBounds(Rect drawBounds) {
setDrawBounds(drawBounds.left, drawBounds.top, drawBounds.right, drawBounds.bottom);
}
public int getWidth() {
return drawBounds.width();
}
public int getHeight() {
return drawBounds.height();
}
public int centerX() {
return drawBounds.centerX();
}
public int centerY() {
return drawBounds.centerY();
}
public float exactCenterX() {
return drawBounds.exactCenterX();
}
public float exactCenterY() {
return drawBounds.exactCenterY();
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/LabelView.java
================================================
package io.virtualapp.widgets;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import io.virtualapp.R;
public class LabelView extends View {
private static final int DEFAULT_DEGREES = 45;
private String mTextContent;
private int mTextColor;
private float mTextSize;
private boolean mTextBold;
private boolean mFillTriangle;
private boolean mTextAllCaps;
private int mBackgroundColor;
private float mMinSize;
private float mPadding;
private int mGravity;
private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Path mPath = new Path();
public LabelView(Context context) {
this(context, null);
}
public LabelView(Context context, AttributeSet attrs) {
super(context, attrs);
obtainAttributes(context, attrs);
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
private void obtainAttributes(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LabelView);
mTextContent = ta.getString(R.styleable.LabelView_lv_text);
mTextColor = ta.getColor(R.styleable.LabelView_lv_text_color, Color.parseColor("#ffffff"));
mTextSize = ta.getDimension(R.styleable.LabelView_lv_text_size, sp2px(11));
mTextBold = ta.getBoolean(R.styleable.LabelView_lv_text_bold, true);
mTextAllCaps = ta.getBoolean(R.styleable.LabelView_lv_text_all_caps, true);
mFillTriangle = ta.getBoolean(R.styleable.LabelView_lv_fill_triangle, false);
mBackgroundColor = ta.getColor(R.styleable.LabelView_lv_background_color, Color.parseColor("#FF4081"));
mMinSize = ta.getDimension(R.styleable.LabelView_lv_min_size, mFillTriangle ? dp2px(35) : dp2px(50));
mPadding = ta.getDimension(R.styleable.LabelView_lv_padding, dp2px(3.5f));
mGravity = ta.getInt(R.styleable.LabelView_lv_gravity, Gravity.TOP | Gravity.LEFT);
ta.recycle();
}
public String getText() {
return mTextContent;
}
public void setText(String text) {
mTextContent = text;
invalidate();
}
public int getTextColor() {
return mTextColor;
}
public void setTextColor(int textColor) {
mTextColor = textColor;
invalidate();
}
public float getTextSize() {
return mTextSize;
}
public void setTextSize(float textSize) {
mTextSize = sp2px(textSize);
invalidate();
}
public boolean isTextBold() {
return mTextBold;
}
public void setTextBold(boolean textBold) {
mTextBold = textBold;
invalidate();
}
public boolean isFillTriangle() {
return mFillTriangle;
}
public void setFillTriangle(boolean fillTriangle) {
mFillTriangle = fillTriangle;
invalidate();
}
public boolean isTextAllCaps() {
return mTextAllCaps;
}
public void setTextAllCaps(boolean textAllCaps) {
mTextAllCaps = textAllCaps;
invalidate();
}
public int getBgColor() {
return mBackgroundColor;
}
public void setBgColor(int backgroundColor) {
mBackgroundColor = backgroundColor;
invalidate();
}
public float getMinSize() {
return mMinSize;
}
public void setMinSize(float minSize) {
mMinSize = dp2px(minSize);
invalidate();
}
public float getPadding() {
return mPadding;
}
public void setPadding(float padding) {
mPadding = dp2px(padding);
invalidate();
}
public int getGravity() {
return mGravity;
}
/**
* Gravity.TOP | Gravity.LEFT
* Gravity.TOP | Gravity.RIGHT
* Gravity.BOTTOM | Gravity.LEFT
* Gravity.BOTTOM | Gravity.RIGHT
*/
public void setGravity(int gravity) {
mGravity = gravity;
}
@Override
protected void onDraw(Canvas canvas) {
int size = getHeight();
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setFakeBoldText(mTextBold);
mBackgroundPaint.setColor(mBackgroundColor);
float textHeight = mTextPaint.descent() - mTextPaint.ascent();
if (mFillTriangle) {
if (mGravity == (Gravity.TOP | Gravity.LEFT)) {
mPath.reset();
mPath.moveTo(0, 0);
mPath.lineTo(0, size);
mPath.lineTo(size, 0);
mPath.close();
canvas.drawPath(mPath, mBackgroundPaint);
drawTextWhenFill(size, -DEFAULT_DEGREES, canvas, true);
} else if (mGravity == (Gravity.TOP | Gravity.RIGHT)) {
mPath.reset();
mPath.moveTo(size, 0);
mPath.lineTo(0, 0);
mPath.lineTo(size, size);
mPath.close();
canvas.drawPath(mPath, mBackgroundPaint);
drawTextWhenFill(size, DEFAULT_DEGREES, canvas, true);
} else if (mGravity == (Gravity.BOTTOM | Gravity.LEFT)) {
mPath.reset();
mPath.moveTo(0, size);
mPath.lineTo(0, 0);
mPath.lineTo(size, size);
mPath.close();
canvas.drawPath(mPath, mBackgroundPaint);
drawTextWhenFill(size, DEFAULT_DEGREES, canvas, false);
} else if (mGravity == (Gravity.BOTTOM | Gravity.RIGHT)) {
mPath.reset();
mPath.moveTo(size, size);
mPath.lineTo(0, size);
mPath.lineTo(size, 0);
mPath.close();
canvas.drawPath(mPath, mBackgroundPaint);
drawTextWhenFill(size, -DEFAULT_DEGREES, canvas, false);
}
} else {
double delta = (textHeight + mPadding * 2) * Math.sqrt(2);
if (mGravity == (Gravity.TOP | Gravity.LEFT)) {
mPath.reset();
mPath.moveTo(0, (float) (size - delta));
mPath.lineTo(0, size);
mPath.lineTo(size, 0);
mPath.lineTo((float) (size - delta), 0);
mPath.close();
canvas.drawPath(mPath, mBackgroundPaint);
drawText(size, -DEFAULT_DEGREES, canvas, textHeight, true);
} else if (mGravity == (Gravity.TOP | Gravity.RIGHT)) {
mPath.reset();
mPath.moveTo(0, 0);
mPath.lineTo((float) delta, 0);
mPath.lineTo(size, (float) (size - delta));
mPath.lineTo(size, size);
mPath.close();
canvas.drawPath(mPath, mBackgroundPaint);
drawText(size, DEFAULT_DEGREES, canvas, textHeight, true);
} else if (mGravity == (Gravity.BOTTOM | Gravity.LEFT)) {
mPath.reset();
mPath.moveTo(0, 0);
mPath.lineTo(0, (float) delta);
mPath.lineTo((float) (size - delta), size);
mPath.lineTo(size, size);
mPath.close();
canvas.drawPath(mPath, mBackgroundPaint);
drawText(size, DEFAULT_DEGREES, canvas, textHeight, false);
} else if (mGravity == (Gravity.BOTTOM | Gravity.RIGHT)) {
mPath.reset();
mPath.moveTo(0, size);
mPath.lineTo((float) delta, size);
mPath.lineTo(size, (float) delta);
mPath.lineTo(size, 0);
mPath.close();
canvas.drawPath(mPath, mBackgroundPaint);
drawText(size, -DEFAULT_DEGREES, canvas, textHeight, false);
}
}
}
private void drawText(int size, float degrees, Canvas canvas, float textHeight, boolean isTop) {
canvas.save();
canvas.rotate(degrees, size / 2f, size / 2f);
float delta = isTop ? -(textHeight + mPadding * 2) / 2 : (textHeight + mPadding * 2) / 2;
float textBaseY = size / 2 - (mTextPaint.descent() + mTextPaint.ascent()) / 2 + delta;
canvas.drawText(mTextAllCaps ? mTextContent.toUpperCase() : mTextContent,
getPaddingLeft() + (size - getPaddingLeft() - getPaddingRight()) / 2, textBaseY, mTextPaint);
canvas.restore();
}
private void drawTextWhenFill(int size, float degrees, Canvas canvas, boolean isTop) {
canvas.save();
canvas.rotate(degrees, size / 2f, size / 2f);
float delta = isTop ? -size / 4 : size / 4;
float textBaseY = size / 2 - (mTextPaint.descent() + mTextPaint.ascent()) / 2 + delta;
canvas.drawText(mTextAllCaps ? mTextContent.toUpperCase() : mTextContent,
getPaddingLeft() + (size - getPaddingLeft() - getPaddingRight()) / 2, textBaseY, mTextPaint);
canvas.restore();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measuredWidth = measureWidth(widthMeasureSpec);
setMeasuredDimension(measuredWidth, measuredWidth);
}
/**
* 确定View宽度大小
*/
private int measureWidth(int widthMeasureSpec) {
int result;
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {//大小确定直接使用
result = specSize;
} else {
int padding = getPaddingLeft() + getPaddingRight();
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);
float textWidth = mTextPaint.measureText(mTextContent + "");
result = (int) ((padding + (int) textWidth) * Math.sqrt(2));
//如果父视图的测量要求为AT_MOST,即限定了一个最大值,则再从系统建议值和自己计算值中去一个较小值
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
result = Math.max((int) mMinSize, result);
}
return result;
}
protected int dp2px(float dp) {
final float scale = getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5f);
}
protected int sp2px(float sp) {
final float scale = getResources().getDisplayMetrics().scaledDensity;
return (int) (sp * scale + 0.5f);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/LauncherIconView.java
================================================
package io.virtualapp.widgets;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.animation.DecelerateInterpolator;
import io.virtualapp.R;
import static android.graphics.Canvas.ALL_SAVE_FLAG;
public class LauncherIconView extends AppCompatImageView implements ShimmerViewBase {
private static final int SMOOTH_ANIM_THRESHOLD = 5;
private static final String TAG = "LauncherIconView";
private ShimmerViewHelper mShimmerViewHelper;
private Shimmer mShimmer;
private float mProgress;
private int mHeight;
private int mWidth;
private int mStrokeWidth;
private float mRadius;
private float mInterDelta;
private int mMaskColor;
private float mMaxMaskRadius;
private float mMaskAnimDelta;
private boolean mIsSquare;
private boolean mMaskAnimRunning;
private long mMediumAnimTime;
private Paint mShimmerPaint;
private Paint mPaint;
private RectF mProgressOval;
private ValueAnimator mInterAnim;
private ValueAnimator mProgressAnimator;
public LauncherIconView(Context context) {
super(context);
init(context, null);
}
public LauncherIconView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public LauncherIconView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mMediumAnimTime = getContext().getResources().getInteger(android.R.integer.config_mediumAnimTime);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressImageView);
try {
this.mProgress = a.getInteger(R.styleable.ProgressImageView_pi_progress, 0);
this.mStrokeWidth = a.getDimensionPixelOffset(R.styleable.ProgressImageView_pi_stroke, 8);
this.mRadius = a.getDimensionPixelOffset(R.styleable.ProgressImageView_pi_radius, 0);
this.mIsSquare = a.getBoolean(R.styleable.ProgressImageView_pi_force_square, false);
this.mMaskColor = a.getColor(R.styleable.ProgressImageView_pi_mask_color, Color.argb(180, 0, 0, 0));
this.mPaint = new Paint();
mPaint.setColor(mMaskColor);
mPaint.setAntiAlias(true);
this.mShimmerPaint = new Paint();
mShimmerPaint.setColor(Color.WHITE);
} finally {
a.recycle();
}
mShimmerViewHelper = new ShimmerViewHelper(this, mShimmerPaint, attrs);
}
private void initParams() {
if (mWidth == 0)
mWidth = getWidth();
if (mHeight == 0)
mHeight = getHeight();
if (mWidth != 0 && mHeight != 0) {
if (mRadius == 0)
mRadius = Math.min(mWidth, mHeight) / 4f;
if (mMaxMaskRadius == 0)
mMaxMaskRadius = (float) (0.5f * Math.sqrt(mWidth * mWidth + mHeight * mHeight));
if (mProgressOval == null)
mProgressOval = new RectF(
mWidth / 2f - mRadius + mStrokeWidth,
mHeight / 2f - mRadius + mStrokeWidth,
mWidth / 2f + mRadius - mStrokeWidth,
mHeight / 2f + mRadius - mStrokeWidth);
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mShimmerViewHelper != null) {
mShimmerViewHelper.onDraw();
}
super.onDraw(canvas);
int sc = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, ALL_SAVE_FLAG);
initParams();
if (mProgress < 100) {
drawMask(canvas);
if (mProgress == 0)
updateInterAnim(canvas);
else
drawProgress(canvas);
}
if (mMaskAnimRunning)
updateMaskAnim(canvas);
canvas.restoreToCount(sc);
}
private void drawMask(Canvas canvas) {
canvas.drawRect(0, 0, mWidth, mHeight, mPaint);
}
private void drawProgress(Canvas canvas) {
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
canvas.drawCircle(mWidth / 2f, mHeight / 2f, mRadius, mPaint);
mPaint.setXfermode(null);
//start angle : -90 ~ 270;sweep Angle : 360 ~ 0;
canvas.drawArc(mProgressOval, -90 + mProgress * 3.6f, 360 - mProgress * 3.6f, true, mPaint);
}
private void updateInterAnim(Canvas canvas) {
// if (!mInterAnimRunning) mInterDelta = 0.f;
//outer circle
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
canvas.drawCircle(mWidth / 2.f, mHeight / 2.f, mRadius, mPaint);
mPaint.setXfermode(null);
//inner circle
canvas.drawCircle(mWidth / 2.f, mHeight / 2.f, mRadius - mInterDelta, mPaint);
}
private void updateMaskAnim(Canvas canvas) {
canvas.drawRect(0, 0, mWidth, mHeight, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
canvas.drawCircle(mWidth / 2f, mHeight / 2f, mRadius + mMaskAnimDelta, mPaint);//mRatio : 0 ~ mRatio * 1.5
mPaint.setXfermode(null);
}
private void startInterAnim(final int progress) {
if (mInterAnim != null)
mInterAnim.cancel();
mInterAnim = ValueAnimator.ofFloat(0.f, mStrokeWidth);
mInterAnim.setInterpolator(new DecelerateInterpolator());
mInterAnim.setDuration(getContext().getResources().getInteger(android.R.integer.config_shortAnimTime));
mInterAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mInterDelta = (float) animation.getAnimatedValue();
invalidate();
}
});
mInterAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
// mInterAnimRunning = true;
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
// mInterAnimRunning = false;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
// mInterAnimRunning = false;
if (progress > 0)
startProgressAnim(0, progress);
}
});
mInterAnim.start();
}
private void startProgressAnim(float from, float to) {
if (mProgressAnimator != null)
mProgressAnimator.cancel();
final boolean isReverse = from > to;
mProgressAnimator = ValueAnimator.ofFloat(from, to);
mProgressAnimator.setInterpolator(new DecelerateInterpolator());
mProgressAnimator.setDuration(mMediumAnimTime);
mProgressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mProgress = (float) animation.getAnimatedValue();
if (0 < mProgress && mProgress < 100)
invalidate();
else if (mProgress == 100 && !isReverse)
startMaskAnim();
}
});
mProgressAnimator.start();
}
private void startMaskAnim() {
if (mProgressAnimator != null)
mProgressAnimator.cancel();
ValueAnimator animator = ValueAnimator.ofFloat(0.f, mMaxMaskRadius);
animator.setInterpolator(new DecelerateInterpolator());
animator.setDuration(mMediumAnimTime);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mMaskAnimRunning = true;
mMaskAnimDelta = (float) animation.getAnimatedValue();
invalidate();
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mMaskAnimRunning = true;
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
mMaskAnimRunning = false;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mMaskAnimRunning = false;
}
});
animator.start();
}
/**
* get the stroke width.
*
* @return the stroke width in pixel.
*/
public int getStrokeWidth() {
return mStrokeWidth;
}
/**
* set the stroke width.default is 8dp.
*
* @param strokeWidth stroke width in pixel
*/
public void setStrokeWidth(int strokeWidth) {
this.mStrokeWidth = strokeWidth;
this.mProgressOval = null;
invalidate();
}
/**
* get the radius of inner progress circle.
*
* @return the inner circle radius in pixel.
*/
public float getRadius() {
return mRadius;
}
/**
* set the radius of the inner progress circle.
*
* @param radius radius in pixel
*/
public void setRadius(float radius) {
this.mRadius = radius;
this.mProgressOval = null;
invalidate();
}
/**
* get the color for mask .
*
* @return the mask color
*/
public int getMaskColor() {
return mMaskColor;
}
/**
* set the color for mask. Argb will looks better. Default is Color.argb(180,0,0,0)
*
* @param maskColor the color value.
*/
public void setMaskColor(int maskColor) {
mMaskColor = maskColor;
mPaint.setColor(mMaskColor);
invalidate();
}
/**
* get current progress.
*
* @return current progress value.
*/
public int getProgress() {
return (int) mProgress;
}
/**
* @param progress the progress ,range [0,100]
*/
public void setProgress(int progress) {
setProgress(progress, true);
}
/**
* @param progress the progress in [0,100]
* @param animate true to enable smooth animation when progress changed more than 5.
*/
public void setProgress(int progress, boolean animate) {
progress = Math.min(Math.max(progress, 0), 100);
Log.d(TAG, "setProgress: p:" + progress + ",mp:" + mProgress);
if (Math.abs(progress - mProgress) > SMOOTH_ANIM_THRESHOLD && animate) {
if (mProgress == 0) {
startInterAnim(progress);
} else {
startProgressAnim(mProgress, progress);
}
} else if (progress == 100 && animate) {
mProgress = 100;
startMaskAnim();
} else {
mProgress = progress;
if (mProgress == 0.f)
mInterDelta = 0.f;
invalidate();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mIsSquare) {
int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
int size = measuredWidth == 0 ? MeasureSpec.getSize(heightMeasureSpec) : measuredWidth;
setMeasuredDimension(size, size);
}
}
@Override
public float getGradientX() {
return mShimmerViewHelper.getGradientX();
}
@Override
public void setGradientX(float gradientX) {
mShimmerViewHelper.setGradientX(gradientX);
}
@Override
public boolean isShimmering() {
return mShimmerViewHelper.isShimmering();
}
@Override
public void setShimmering(boolean isShimmering) {
mShimmerViewHelper.setShimmering(isShimmering);
}
@Override
public boolean isSetUp() {
return mShimmerViewHelper.isSetUp();
}
@Override
public void setAnimationSetupCallback(ShimmerViewHelper.AnimationSetupCallback callback) {
mShimmerViewHelper.setAnimationSetupCallback(callback);
}
@Override
public int getPrimaryColor() {
return mShimmerViewHelper.getPrimaryColor();
}
@Override
public void setPrimaryColor(int primaryColor) {
mShimmerViewHelper.setPrimaryColor(primaryColor);
}
@Override
public int getReflectionColor() {
return mShimmerViewHelper.getReflectionColor();
}
@Override
public void setReflectionColor(int reflectionColor) {
mShimmerViewHelper.setReflectionColor(reflectionColor);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mShimmerViewHelper != null) {
mShimmerViewHelper.onSizeChanged();
}
}
public void stopShimmer() {
if (mShimmer != null && mShimmer.isAnimating()) {
mShimmer.cancel();
mShimmer = null;
}
}
public void startShimmer() {
stopShimmer();
mShimmer = new Shimmer();
mShimmer.setRepeatCount(1)
.setStartDelay(800L)
.setDirection(Shimmer.ANIMATION_DIRECTION_LTR)
.start(this);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/LoadingIndicatorView.java
================================================
package io.virtualapp.widgets;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.AnimationUtils;
import io.virtualapp.R;
public class LoadingIndicatorView extends View {
private static final String TAG = "LoadingIndicatorView";
private static final Indicator DEFAULT_INDICATOR = new BallGridBeatIndicator();
private static final int MIN_SHOW_TIME = 500; // ms
private static final int MIN_DELAY = 500; // ms
int mMinWidth;
int mMaxWidth;
int mMinHeight;
int mMaxHeight;
private long mStartTime = -1;
private boolean mPostedHide = false;
private boolean mPostedShow = false;
private boolean mDismissed = false;
private Indicator mIndicator;
private int mIndicatorColor;
private boolean mShouldStartAnimationDrawable;
private final Runnable mDelayedHide = new Runnable() {
@Override
public void run() {
mPostedHide = false;
mStartTime = -1;
setVisibility(View.GONE);
}
};
private final Runnable mDelayedShow = new Runnable() {
@Override
public void run() {
mPostedShow = false;
if (!mDismissed) {
mStartTime = System.currentTimeMillis();
setVisibility(View.VISIBLE);
}
}
};
public LoadingIndicatorView(Context context) {
super(context);
init(context, null, 0, 0);
}
public LoadingIndicatorView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0, R.style.AVLoadingIndicatorView);
}
public LoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, R.style.AVLoadingIndicatorView);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public LoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr, R.style.AVLoadingIndicatorView);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
mMinWidth = 24;
mMaxWidth = 48;
mMinHeight = 24;
mMaxHeight = 48;
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.LoadingIndicatorView, defStyleAttr, defStyleRes);
mMinWidth = a.getDimensionPixelSize(R.styleable.LoadingIndicatorView_minWidth, mMinWidth);
mMaxWidth = a.getDimensionPixelSize(R.styleable.LoadingIndicatorView_maxWidth, mMaxWidth);
mMinHeight = a.getDimensionPixelSize(R.styleable.LoadingIndicatorView_minHeight, mMinHeight);
mMaxHeight = a.getDimensionPixelSize(R.styleable.LoadingIndicatorView_maxHeight, mMaxHeight);
String indicatorName = a.getString(R.styleable.LoadingIndicatorView_indicatorName);
mIndicatorColor = a.getColor(R.styleable.LoadingIndicatorView_indicatorColor, Color.WHITE);
setIndicator(indicatorName);
if (mIndicator == null) {
setIndicator(DEFAULT_INDICATOR);
}
a.recycle();
}
public Indicator getIndicator() {
return mIndicator;
}
/**
* You should pay attention to pass this parameter with two way:
* for example:
* 1. Only class Name,like "SimpleIndicator".(This way would use default package name with
* "com.wang.avi.indicators")
* 2. Class name with full package,like "com.my.android.indicators.SimpleIndicator".
*
* @param indicatorName the class must be extend Indicator .
*/
public void setIndicator(String indicatorName) {
if (TextUtils.isEmpty(indicatorName)) {
return;
}
StringBuilder drawableClassName = new StringBuilder();
if (!indicatorName.contains(".")) {
String defaultPackageName = getClass().getPackage().getName();
drawableClassName.append(defaultPackageName)
.append(".");
}
drawableClassName.append(indicatorName);
try {
Class> drawableClass = Class.forName(drawableClassName.toString());
Indicator indicator = (Indicator) drawableClass.newInstance();
setIndicator(indicator);
} catch (ClassNotFoundException e) {
Log.e(TAG, "Didn't find your class , check the name again !");
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public void setIndicator(Indicator d) {
if (mIndicator != d) {
if (mIndicator != null) {
mIndicator.setCallback(null);
unscheduleDrawable(mIndicator);
}
mIndicator = d;
//need to set indicator color again if you didn't specified when you update the indicator .
setIndicatorColor(mIndicatorColor);
if (d != null) {
d.setCallback(this);
}
postInvalidate();
}
}
/**
* setIndicatorColor(0xFF00FF00)
* or
* setIndicatorColor(Color.BLUE)
* or
* setIndicatorColor(Color.parseColor("#FF4081"))
* or
* setIndicatorColor(0xFF00FF00)
* or
* setIndicatorColor(getResources().getColor(android.R.color.black))
*
* @param color
*/
public void setIndicatorColor(int color) {
this.mIndicatorColor = color;
mIndicator.setColor(color);
}
public void smoothToShow() {
startAnimation(AnimationUtils.loadAnimation(getContext(), android.R.anim.fade_in));
setVisibility(VISIBLE);
}
public void smoothToHide() {
startAnimation(AnimationUtils.loadAnimation(getContext(), android.R.anim.fade_out));
setVisibility(GONE);
}
public void hide() {
mDismissed = true;
removeCallbacks(mDelayedShow);
long diff = System.currentTimeMillis() - mStartTime;
if (diff >= MIN_SHOW_TIME || mStartTime == -1) {
// The progress spinner has been shown long enough
// OR was not shown yet. If it wasn't shown yet,
// it will just never be shown.
setVisibility(View.GONE);
} else {
// The progress spinner is shown, but not long enough,
// so put a delayed message in to hide it when its been
// shown long enough.
if (!mPostedHide) {
postDelayed(mDelayedHide, MIN_SHOW_TIME - diff);
mPostedHide = true;
}
}
}
public void show() {
// Reset the start time.
mStartTime = -1;
mDismissed = false;
removeCallbacks(mDelayedHide);
if (!mPostedShow) {
postDelayed(mDelayedShow, MIN_DELAY);
mPostedShow = true;
}
}
@Override
protected boolean verifyDrawable(Drawable who) {
return who == mIndicator
|| super.verifyDrawable(who);
}
void startAnimation() {
if (getVisibility() != VISIBLE) {
return;
}
if (mIndicator instanceof Animatable) {
mShouldStartAnimationDrawable = true;
}
postInvalidate();
}
void stopAnimation() {
if (mIndicator instanceof Animatable) {
mIndicator.stop();
mShouldStartAnimationDrawable = false;
}
postInvalidate();
}
@Override
public void setVisibility(int v) {
if (getVisibility() != v) {
super.setVisibility(v);
if (v == GONE || v == INVISIBLE) {
stopAnimation();
} else {
startAnimation();
}
}
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (visibility == GONE || visibility == INVISIBLE) {
stopAnimation();
} else {
startAnimation();
}
}
@Override
public void invalidateDrawable(Drawable dr) {
if (verifyDrawable(dr)) {
final Rect dirty = dr.getBounds();
final int scrollX = getScrollX() + getPaddingLeft();
final int scrollY = getScrollY() + getPaddingTop();
invalidate(dirty.left + scrollX, dirty.top + scrollY,
dirty.right + scrollX, dirty.bottom + scrollY);
} else {
super.invalidateDrawable(dr);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
updateDrawableBounds(w, h);
}
private void updateDrawableBounds(int w, int h) {
// onDraw will translate the canvas so we draw starting at 0,0.
// Subtract out padding for the purposes of the calculations below.
w -= getPaddingRight() + getPaddingLeft();
h -= getPaddingTop() + getPaddingBottom();
int right = w;
int bottom = h;
int top = 0;
int left = 0;
if (mIndicator != null) {
// Maintain aspect ratio. Certain kinds of animated drawables
// get very confused otherwise.
final int intrinsicWidth = mIndicator.getIntrinsicWidth();
final int intrinsicHeight = mIndicator.getIntrinsicHeight();
final float intrinsicAspect = (float) intrinsicWidth / intrinsicHeight;
final float boundAspect = (float) w / h;
if (intrinsicAspect != boundAspect) {
if (boundAspect > intrinsicAspect) {
// New width is larger. Make it smaller to match height.
final int width = (int) (h * intrinsicAspect);
left = (w - width) / 2;
right = left + width;
} else {
// New height is larger. Make it smaller to match width.
final int height = (int) (w * (1 / intrinsicAspect));
top = (h - height) / 2;
bottom = top + height;
}
}
mIndicator.setBounds(left, top, right, bottom);
}
}
@Override
protected synchronized void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawTrack(canvas);
}
void drawTrack(Canvas canvas) {
final Drawable d = mIndicator;
if (d != null) {
// Translate canvas so a indeterminate circular progress bar with padding
// rotates properly in its animation
final int saveCount = canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop());
d.draw(canvas);
canvas.restoreToCount(saveCount);
if (mShouldStartAnimationDrawable && d instanceof Animatable) {
((Animatable) d).start();
mShouldStartAnimationDrawable = false;
}
}
}
@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int dw = 0;
int dh = 0;
final Drawable d = mIndicator;
if (d != null) {
dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
}
updateDrawableState();
dw += getPaddingLeft() + getPaddingRight();
dh += getPaddingTop() + getPaddingBottom();
final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0);
final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0);
setMeasuredDimension(measuredWidth, measuredHeight);
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
updateDrawableState();
}
private void updateDrawableState() {
final int[] state = getDrawableState();
if (mIndicator != null && mIndicator.isStateful()) {
mIndicator.setState(state);
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void drawableHotspotChanged(float x, float y) {
super.drawableHotspotChanged(x, y);
if (mIndicator != null) {
mIndicator.setHotspot(x, y);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
startAnimation();
removeCallbacks();
}
@Override
protected void onDetachedFromWindow() {
stopAnimation();
// This should come after stopAnimation(), otherwise an invalidate message remains in the
// queue, which can prevent the entire view hierarchy from being GC'ed during a rotation
super.onDetachedFromWindow();
removeCallbacks();
}
private void removeCallbacks() {
removeCallbacks(mDelayedHide);
removeCallbacks(mDelayedShow);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/MarqueeTextView.java
================================================
package io.virtualapp.widgets;
import android.content.Context;
import android.graphics.Canvas;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
public class MarqueeTextView extends AppCompatTextView {
private boolean isStop = false;
public MarqueeTextView(Context context) {
super(context);
}
public MarqueeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MarqueeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public boolean isFocused() {
if (this.isStop) {
return super.isFocused();
}
return true;
}
public void stopScroll() {
this.isStop = true;
}
public void start() {
this.isStop = false;
}
protected void onDetachedFromWindow() {
stopScroll();
super.onDetachedFromWindow();
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/MaterialRippleLayout.java
================================================
package io.virtualapp.widgets;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Property;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import io.virtualapp.R;
import static android.view.GestureDetector.SimpleOnGestureListener;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
public class MaterialRippleLayout extends FrameLayout {
private static final int DEFAULT_DURATION = 350;
private static final int DEFAULT_FADE_DURATION = 75;
private static final float DEFAULT_DIAMETER_DP = 35;
private static final float DEFAULT_ALPHA = 0.2f;
private static final int DEFAULT_COLOR = Color.BLACK;
private static final int DEFAULT_BACKGROUND = Color.TRANSPARENT;
private static final boolean DEFAULT_HOVER = true;
private static final boolean DEFAULT_DELAY_CLICK = true;
private static final boolean DEFAULT_PERSISTENT = false;
private static final boolean DEFAULT_SEARCH_ADAPTER = false;
private static final boolean DEFAULT_RIPPLE_OVERLAY = false;
private static final int DEFAULT_ROUNDED_CORNERS = 0;
private static final int FADE_EXTRA_DELAY = 50;
private static final long HOVER_DURATION = 2500;
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Rect bounds = new Rect();
private int rippleColor;
private boolean rippleOverlay;
private boolean rippleHover;
private int rippleDiameter;
private int rippleDuration;
private int rippleAlpha;
private boolean rippleDelayClick;
private int rippleFadeDuration;
private boolean ripplePersistent;
private Drawable rippleBackground;
private boolean rippleInAdapter;
private float rippleRoundedCorners;
private float radius;
private AdapterView parentAdapter;
private View childView;
private AnimatorSet rippleAnimator;
private ObjectAnimator hoverAnimator;
private Point currentCoords = new Point();
private Point previousCoords = new Point();
private int layerType;
private boolean eventCancelled;
private boolean prepressed;
private int positionInAdapter;
private GestureDetector gestureDetector;
private PerformClickEvent pendingClickEvent;
private PressedEvent pendingPressEvent;
private boolean hasPerformedLongPress;
/*
* Animations
*/
private Property radiusProperty
= new Property(Float.class, "radius") {
@Override
public Float get(MaterialRippleLayout object) {
return object.getRadius();
}
@Override
public void set(MaterialRippleLayout object, Float value) {
object.setRadius(value);
}
};
private Property circleAlphaProperty
= new Property(Integer.class, "rippleAlpha") {
@Override
public Integer get(MaterialRippleLayout object) {
return object.getRippleAlpha();
}
@Override
public void set(MaterialRippleLayout object, Integer value) {
object.setRippleAlpha(value);
}
};
private SimpleOnGestureListener longClickListener = new GestureDetector.SimpleOnGestureListener() {
public void onLongPress(MotionEvent e) {
hasPerformedLongPress = childView.performLongClick();
if (hasPerformedLongPress) {
if (rippleHover) {
startRipple(null);
}
cancelPressedEvent();
}
}
@Override
public boolean onDown(MotionEvent e) {
hasPerformedLongPress = false;
return super.onDown(e);
}
};
public MaterialRippleLayout(Context context) {
this(context, null, 0);
}
public MaterialRippleLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MaterialRippleLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setWillNotDraw(false);
gestureDetector = new GestureDetector(context, longClickListener);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MaterialRippleLayout);
rippleColor = a.getColor(R.styleable.MaterialRippleLayout_mrl_rippleColor, DEFAULT_COLOR);
rippleDiameter = a.getDimensionPixelSize(
R.styleable.MaterialRippleLayout_mrl_rippleDimension,
(int) dpToPx(getResources(), DEFAULT_DIAMETER_DP)
);
rippleOverlay = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleOverlay, DEFAULT_RIPPLE_OVERLAY);
rippleHover = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleHover, DEFAULT_HOVER);
rippleDuration = a.getInt(R.styleable.MaterialRippleLayout_mrl_rippleDuration, DEFAULT_DURATION);
rippleAlpha = (int) (255 * a.getFloat(R.styleable.MaterialRippleLayout_mrl_rippleAlpha, DEFAULT_ALPHA));
rippleDelayClick = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleDelayClick, DEFAULT_DELAY_CLICK);
rippleFadeDuration = a.getInteger(R.styleable.MaterialRippleLayout_mrl_rippleFadeDuration, DEFAULT_FADE_DURATION);
rippleBackground = new ColorDrawable(a.getColor(R.styleable.MaterialRippleLayout_mrl_rippleBackground, DEFAULT_BACKGROUND));
ripplePersistent = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_ripplePersistent, DEFAULT_PERSISTENT);
rippleInAdapter = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleInAdapter, DEFAULT_SEARCH_ADAPTER);
rippleRoundedCorners = a.getDimensionPixelSize(R.styleable.MaterialRippleLayout_mrl_rippleRoundedCorners, DEFAULT_ROUNDED_CORNERS);
a.recycle();
paint.setColor(rippleColor);
paint.setAlpha(rippleAlpha);
enableClipPathSupportIfNecessary();
}
public static RippleBuilder on(View view) {
return new RippleBuilder(view);
}
static float dpToPx(Resources resources, float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.getDisplayMetrics());
}
@SuppressWarnings("unchecked")
public T getChildView() {
return (T) childView;
}
@Override
public final void addView(View child, int index, ViewGroup.LayoutParams params) {
if (getChildCount() > 0) {
throw new IllegalStateException("MaterialRippleLayout can host only one child");
}
//noinspection unchecked
childView = child;
super.addView(child, index, params);
}
@Override
public void setOnClickListener(OnClickListener onClickListener) {
if (childView == null) {
throw new IllegalStateException("MaterialRippleLayout must have a child view to handle clicks");
}
childView.setOnClickListener(onClickListener);
}
@Override
public void setOnLongClickListener(OnLongClickListener onClickListener) {
if (childView == null) {
throw new IllegalStateException("MaterialRippleLayout must have a child view to handle clicks");
}
childView.setOnLongClickListener(onClickListener);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return !findClickableViewInChild(childView, (int) event.getX(), (int) event.getY());
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean superOnTouchEvent = super.onTouchEvent(event);
if (!isEnabled() || !childView.isEnabled()) return superOnTouchEvent;
boolean isEventInBounds = bounds.contains((int) event.getX(), (int) event.getY());
if (isEventInBounds) {
previousCoords.set(currentCoords.x, currentCoords.y);
currentCoords.set((int) event.getX(), (int) event.getY());
}
boolean gestureResult = gestureDetector.onTouchEvent(event);
if (gestureResult || hasPerformedLongPress) {
return true;
} else {
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_UP:
pendingClickEvent = new PerformClickEvent();
if (prepressed) {
childView.setPressed(true);
postDelayed(
new Runnable() {
@Override public void run() {
childView.setPressed(false);
}
}, ViewConfiguration.getPressedStateDuration());
}
if (isEventInBounds) {
startRipple(pendingClickEvent);
} else if (!rippleHover) {
setRadius(0);
}
if (!rippleDelayClick && isEventInBounds) {
pendingClickEvent.run();
}
cancelPressedEvent();
break;
case MotionEvent.ACTION_DOWN:
setPositionInAdapter();
eventCancelled = false;
pendingPressEvent = new PressedEvent(event);
if (isInScrollingContainer()) {
cancelPressedEvent();
prepressed = true;
postDelayed(pendingPressEvent, ViewConfiguration.getTapTimeout());
} else {
pendingPressEvent.run();
}
break;
case MotionEvent.ACTION_CANCEL:
if (rippleInAdapter) {
// dont use current coords in adapter since they tend to jump drastically on scroll
currentCoords.set(previousCoords.x, previousCoords.y);
previousCoords = new Point();
}
childView.onTouchEvent(event);
if (rippleHover) {
if (!prepressed) {
startRipple(null);
}
} else {
childView.setPressed(false);
}
cancelPressedEvent();
break;
case MotionEvent.ACTION_MOVE:
if (rippleHover) {
if (isEventInBounds && !eventCancelled) {
invalidate();
} else if (!isEventInBounds) {
startRipple(null);
}
}
if (!isEventInBounds) {
cancelPressedEvent();
if (hoverAnimator != null) {
hoverAnimator.cancel();
}
childView.onTouchEvent(event);
eventCancelled = true;
}
break;
}
return true;
}
}
private void cancelPressedEvent() {
if (pendingPressEvent != null) {
removeCallbacks(pendingPressEvent);
prepressed = false;
}
}
private void startHover() {
if (eventCancelled) return;
if (hoverAnimator != null) {
hoverAnimator.cancel();
}
final float radius = (float) (Math.sqrt(Math.pow(getWidth(), 2) + Math.pow(getHeight(), 2)) * 1.2f);
hoverAnimator = ObjectAnimator.ofFloat(this, radiusProperty, rippleDiameter, radius)
.setDuration(HOVER_DURATION);
hoverAnimator.setInterpolator(new LinearInterpolator());
hoverAnimator.start();
}
private void startRipple(final Runnable animationEndRunnable) {
if (eventCancelled) return;
float endRadius = getEndRadius();
cancelAnimations();
rippleAnimator = new AnimatorSet();
rippleAnimator.addListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
if (!ripplePersistent) {
setRadius(0);
setRippleAlpha(rippleAlpha);
}
if (animationEndRunnable != null && rippleDelayClick) {
animationEndRunnable.run();
}
childView.setPressed(false);
}
});
ObjectAnimator ripple = ObjectAnimator.ofFloat(this, radiusProperty, radius, endRadius);
ripple.setDuration(rippleDuration);
ripple.setInterpolator(new DecelerateInterpolator());
ObjectAnimator fade = ObjectAnimator.ofInt(this, circleAlphaProperty, rippleAlpha, 0);
fade.setDuration(rippleFadeDuration);
fade.setInterpolator(new AccelerateInterpolator());
fade.setStartDelay(rippleDuration - rippleFadeDuration - FADE_EXTRA_DELAY);
if (ripplePersistent) {
rippleAnimator.play(ripple);
} else if (getRadius() > endRadius) {
fade.setStartDelay(0);
rippleAnimator.play(fade);
} else {
rippleAnimator.playTogether(ripple, fade);
}
rippleAnimator.start();
}
private void cancelAnimations() {
if (rippleAnimator != null) {
rippleAnimator.cancel();
rippleAnimator.removeAllListeners();
}
if (hoverAnimator != null) {
hoverAnimator.cancel();
}
}
private float getEndRadius() {
final int width = getWidth();
final int height = getHeight();
final int halfWidth = width / 2;
final int halfHeight = height / 2;
final float radiusX = halfWidth > currentCoords.x ? width - currentCoords.x : currentCoords.x;
final float radiusY = halfHeight > currentCoords.y ? height - currentCoords.y : currentCoords.y;
return (float) Math.sqrt(Math.pow(radiusX, 2) + Math.pow(radiusY, 2)) * 1.2f;
}
private boolean isInScrollingContainer() {
ViewParent p = getParent();
while (p != null && p instanceof ViewGroup) {
if (((ViewGroup) p).shouldDelayChildPressedState()) {
return true;
}
p = p.getParent();
}
return false;
}
private AdapterView findParentAdapterView() {
if (parentAdapter != null) {
return parentAdapter;
}
ViewParent current = getParent();
while (true) {
if (current instanceof AdapterView) {
parentAdapter = (AdapterView) current;
return parentAdapter;
} else {
try {
current = current.getParent();
} catch (NullPointerException npe) {
throw new RuntimeException("Could not find a parent AdapterView");
}
}
}
}
private void setPositionInAdapter() {
if (rippleInAdapter) {
positionInAdapter = findParentAdapterView().getPositionForView(MaterialRippleLayout.this);
}
}
private boolean adapterPositionChanged() {
if (rippleInAdapter) {
int newPosition = findParentAdapterView().getPositionForView(MaterialRippleLayout.this);
final boolean changed = newPosition != positionInAdapter;
positionInAdapter = newPosition;
if (changed) {
cancelPressedEvent();
cancelAnimations();
childView.setPressed(false);
setRadius(0);
}
return changed;
}
return false;
}
private boolean findClickableViewInChild(View view, int x, int y) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
final Rect rect = new Rect();
child.getHitRect(rect);
final boolean contains = rect.contains(x, y);
if (contains) {
return findClickableViewInChild(child, x - rect.left, y - rect.top);
}
}
} else if (view != childView) {
return (view.isEnabled() && (view.isClickable() || view.isLongClickable() || view.isFocusableInTouchMode()));
}
return view.isFocusableInTouchMode();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
bounds.set(0, 0, w, h);
rippleBackground.setBounds(bounds);
}
@Override
public boolean isInEditMode() {
return true;
}
/*
* Drawing
*/
@Override
public void draw(Canvas canvas) {
final boolean positionChanged = adapterPositionChanged();
if (rippleOverlay) {
if (!positionChanged) {
rippleBackground.draw(canvas);
}
super.draw(canvas);
if (!positionChanged) {
if (rippleRoundedCorners != 0) {
Path clipPath = new Path();
RectF rect = new RectF(0, 0, canvas.getWidth(), canvas.getHeight());
clipPath.addRoundRect(rect, rippleRoundedCorners, rippleRoundedCorners, Path.Direction.CW);
canvas.clipPath(clipPath);
}
canvas.drawCircle(currentCoords.x, currentCoords.y, radius, paint);
}
} else {
if (!positionChanged) {
rippleBackground.draw(canvas);
canvas.drawCircle(currentCoords.x, currentCoords.y, radius, paint);
}
super.draw(canvas);
}
}
private float getRadius() {
return radius;
}
public void setRadius(float radius) {
this.radius = radius;
invalidate();
}
public int getRippleAlpha() {
return paint.getAlpha();
}
public void setRippleAlpha(Integer rippleAlpha) {
paint.setAlpha(rippleAlpha);
invalidate();
}
/*
* Accessor
*/
public void setRippleColor(int rippleColor) {
this.rippleColor = rippleColor;
paint.setColor(rippleColor);
paint.setAlpha(rippleAlpha);
invalidate();
}
public void setRippleOverlay(boolean rippleOverlay) {
this.rippleOverlay = rippleOverlay;
}
public void setRippleDiameter(int rippleDiameter) {
this.rippleDiameter = rippleDiameter;
}
public void setRippleDuration(int rippleDuration) {
this.rippleDuration = rippleDuration;
}
public void setRippleBackground(int color) {
rippleBackground = new ColorDrawable(color);
rippleBackground.setBounds(bounds);
invalidate();
}
public void setRippleHover(boolean rippleHover) {
this.rippleHover = rippleHover;
}
public void setRippleDelayClick(boolean rippleDelayClick) {
this.rippleDelayClick = rippleDelayClick;
}
public void setRippleFadeDuration(int rippleFadeDuration) {
this.rippleFadeDuration = rippleFadeDuration;
}
public void setRipplePersistent(boolean ripplePersistent) {
this.ripplePersistent = ripplePersistent;
}
public void setRippleInAdapter(boolean rippleInAdapter) {
this.rippleInAdapter = rippleInAdapter;
}
public void setRippleRoundedCorners(int rippleRoundedCorner) {
this.rippleRoundedCorners = rippleRoundedCorner;
enableClipPathSupportIfNecessary();
}
public void setDefaultRippleAlpha(float alpha) {
this.rippleAlpha = (int) (255 * alpha);
paint.setAlpha(rippleAlpha);
invalidate();
}
public void performRipple() {
currentCoords = new Point(getWidth() / 2, getHeight() / 2);
startRipple(null);
}
public void performRipple(Point anchor) {
currentCoords = new Point(anchor.x, anchor.y);
startRipple(null);
}
/**
* {@link Canvas#clipPath(Path)} is not supported in hardware accelerated layers
* before API 18. Use software layer instead
*
* https://developer.android.com/guide/topics/graphics/hardware-accel.html#unsupported
*/
private void enableClipPathSupportIfNecessary() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
if (rippleRoundedCorners != 0) {
layerType = getLayerType();
setLayerType(LAYER_TYPE_SOFTWARE, null);
} else {
setLayerType(layerType, null);
}
}
}
public static class RippleBuilder {
private final Context context;
private final View child;
private int rippleColor = DEFAULT_COLOR;
private boolean rippleOverlay = DEFAULT_RIPPLE_OVERLAY;
private boolean rippleHover = DEFAULT_HOVER;
private float rippleDiameter = DEFAULT_DIAMETER_DP;
private int rippleDuration = DEFAULT_DURATION;
private float rippleAlpha = DEFAULT_ALPHA;
private boolean rippleDelayClick = DEFAULT_DELAY_CLICK;
private int rippleFadeDuration = DEFAULT_FADE_DURATION;
private boolean ripplePersistent = DEFAULT_PERSISTENT;
private int rippleBackground = DEFAULT_BACKGROUND;
private boolean rippleSearchAdapter = DEFAULT_SEARCH_ADAPTER;
private float rippleRoundedCorner = DEFAULT_ROUNDED_CORNERS;
public RippleBuilder(View child) {
this.child = child;
this.context = child.getContext();
}
public RippleBuilder rippleColor(int color) {
this.rippleColor = color;
return this;
}
public RippleBuilder rippleOverlay(boolean overlay) {
this.rippleOverlay = overlay;
return this;
}
public RippleBuilder rippleHover(boolean hover) {
this.rippleHover = hover;
return this;
}
public RippleBuilder rippleDiameterDp(int diameterDp) {
this.rippleDiameter = diameterDp;
return this;
}
public RippleBuilder rippleDuration(int duration) {
this.rippleDuration = duration;
return this;
}
public RippleBuilder rippleAlpha(float alpha) {
this.rippleAlpha = alpha;
return this;
}
public RippleBuilder rippleDelayClick(boolean delayClick) {
this.rippleDelayClick = delayClick;
return this;
}
public RippleBuilder rippleFadeDuration(int fadeDuration) {
this.rippleFadeDuration = fadeDuration;
return this;
}
public RippleBuilder ripplePersistent(boolean persistent) {
this.ripplePersistent = persistent;
return this;
}
public RippleBuilder rippleBackground(int color) {
this.rippleBackground = color;
return this;
}
public RippleBuilder rippleInAdapter(boolean inAdapter) {
this.rippleSearchAdapter = inAdapter;
return this;
}
public RippleBuilder rippleRoundedCorners(int radiusDp) {
this.rippleRoundedCorner = radiusDp;
return this;
}
public MaterialRippleLayout create() {
MaterialRippleLayout layout = new MaterialRippleLayout(context);
layout.setRippleColor(rippleColor);
layout.setDefaultRippleAlpha(rippleAlpha);
layout.setRippleDelayClick(rippleDelayClick);
layout.setRippleDiameter((int) dpToPx(context.getResources(), rippleDiameter));
layout.setRippleDuration(rippleDuration);
layout.setRippleFadeDuration(rippleFadeDuration);
layout.setRippleHover(rippleHover);
layout.setRipplePersistent(ripplePersistent);
layout.setRippleOverlay(rippleOverlay);
layout.setRippleBackground(rippleBackground);
layout.setRippleInAdapter(rippleSearchAdapter);
layout.setRippleRoundedCorners((int) dpToPx(context.getResources(), rippleRoundedCorner));
ViewGroup.LayoutParams params = child.getLayoutParams();
ViewGroup parent = (ViewGroup) child.getParent();
int index = 0;
if (parent != null && parent instanceof MaterialRippleLayout) {
throw new IllegalStateException("MaterialRippleLayout could not be created: parent of the view already is a MaterialRippleLayout");
}
if (parent != null) {
index = parent.indexOfChild(child);
parent.removeView(child);
}
layout.addView(child, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
if (parent != null) {
parent.addView(layout, index, params);
}
return layout;
}
}
/*
* Helper
*/
private class PerformClickEvent implements Runnable {
@Override public void run() {
if (hasPerformedLongPress) return;
// if parent is an AdapterView, try to call its ItemClickListener
if (getParent() instanceof AdapterView) {
// try clicking direct child first
if (!childView.performClick())
// if it did not handle it dispatch to adapterView
clickAdapterView((AdapterView) getParent());
} else if (rippleInAdapter) {
// find adapter view
clickAdapterView(findParentAdapterView());
} else {
// otherwise, just perform click on child
childView.performClick();
}
}
private void clickAdapterView(AdapterView parent) {
final int position = parent.getPositionForView(MaterialRippleLayout.this);
final long itemId = parent.getAdapter() != null
? parent.getAdapter().getItemId(position)
: 0;
if (position != AdapterView.INVALID_POSITION) {
parent.performItemClick(MaterialRippleLayout.this, position, itemId);
}
}
}
/*
* Builder
*/
private final class PressedEvent implements Runnable {
private final MotionEvent event;
public PressedEvent(MotionEvent event) {
this.event = event;
}
@Override
public void run() {
prepressed = false;
childView.setLongClickable(false);//prevent the child's long click,let's the ripple layout call it's performLongClick
childView.onTouchEvent(event);
childView.setPressed(true);
if (rippleHover) {
startHover();
}
}
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/RippleButton.java
================================================
package io.virtualapp.widgets;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.Shader;
import android.os.Build;
import android.support.v7.widget.AppCompatButton;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.view.ViewHelper;
import io.virtualapp.R;
@SuppressLint("ClickableViewAccessibility")
public class RippleButton extends AppCompatButton {
private float mDownX;
private float mDownY;
private float mAlphaFactor;
private float mDensity;
private float mRadius;
private float mMaxRadius;
private int mRippleColor;
private boolean mIsAnimating = false;
private boolean mHover = true;
private RadialGradient mRadialGradient;
private Paint mPaint;
private ObjectAnimator mRadiusAnimator;
private boolean mAnimationIsCancel;
private Rect mRect;
private Path mPath = new Path();
public RippleButton(Context context) {
this(context, null);
}
public RippleButton(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RippleButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.RippleButton);
mRippleColor = a.getColor(R.styleable.RippleButton_rippleColor,
mRippleColor);
mAlphaFactor = a.getFloat(R.styleable.RippleButton_alphaFactor,
mAlphaFactor);
mHover = a.getBoolean(R.styleable.RippleButton_hover, mHover);
a.recycle();
}
private int dp(int dp) {
return (int) (dp * mDensity + 0.5f);
}
public void init() {
mDensity = getContext().getResources().getDisplayMetrics().density;
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAlpha(100);
setRippleColor(Color.BLACK, 0.2f);
}
public void setRippleColor(int rippleColor, float alphaFactor) {
mRippleColor = rippleColor;
mAlphaFactor = alphaFactor;
}
public void setHover(boolean enabled) {
mHover = enabled;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mMaxRadius = (float) Math.sqrt(w * w + h * h);
}
@Override
public boolean onTouchEvent(final MotionEvent event) {
Log.d("TouchEvent", String.valueOf(event.getActionMasked()));
Log.d("mIsAnimating", String.valueOf(mIsAnimating));
Log.d("mAnimationIsCancel", String.valueOf(mAnimationIsCancel));
boolean superResult = super.onTouchEvent(event);
if (event.getActionMasked() == MotionEvent.ACTION_DOWN
&& this.isEnabled() && mHover) {
mRect = new Rect(getLeft(), getTop(), getRight(), getBottom());
mAnimationIsCancel = false;
mDownX = event.getX();
mDownY = event.getY();
mRadiusAnimator = ObjectAnimator.ofFloat(this, "radius", 0, dp(50))
.setDuration(400);
mRadiusAnimator
.setInterpolator(new AccelerateDecelerateInterpolator());
mRadiusAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
mIsAnimating = true;
}
@Override
public void onAnimationEnd(Animator animator) {
setRadius(0);
ViewHelper.setAlpha(RippleButton.this, 1);
mIsAnimating = false;
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
mRadiusAnimator.start();
if (!superResult) {
return true;
}
} else if (event.getActionMasked() == MotionEvent.ACTION_MOVE
&& this.isEnabled() && mHover) {
mDownX = event.getX();
mDownY = event.getY();
// Cancel the ripple animation when moved outside
if (mAnimationIsCancel = !mRect.contains(
getLeft() + (int) event.getX(),
getTop() + (int) event.getY())) {
setRadius(0);
} else {
setRadius(dp(50));
}
if (!superResult) {
return true;
}
} else if (event.getActionMasked() == MotionEvent.ACTION_UP
&& !mAnimationIsCancel && this.isEnabled()) {
mDownX = event.getX();
mDownY = event.getY();
final float tempRadius = (float) Math.sqrt(mDownX * mDownX + mDownY
* mDownY);
float targetRadius = Math.max(tempRadius, mMaxRadius);
if (mIsAnimating) {
mRadiusAnimator.cancel();
}
mRadiusAnimator = ObjectAnimator.ofFloat(this, "radius", dp(50),
targetRadius);
mRadiusAnimator.setDuration(500);
mRadiusAnimator
.setInterpolator(new AccelerateDecelerateInterpolator());
mRadiusAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
mIsAnimating = true;
}
@Override
public void onAnimationEnd(Animator animator) {
setRadius(0);
ViewHelper.setAlpha(RippleButton.this, 1);
mIsAnimating = false;
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
mRadiusAnimator.start();
if (!superResult) {
return true;
}
}
return superResult;
}
public int adjustAlpha(int color, float factor) {
int alpha = Math.round(Color.alpha(color) * factor);
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
return Color.argb(alpha, red, green, blue);
}
public void setRadius(final float radius) {
mRadius = radius;
if (mRadius > 0) {
mRadialGradient = new RadialGradient(mDownX, mDownY, mRadius,
adjustAlpha(mRippleColor, mAlphaFactor), mRippleColor,
Shader.TileMode.MIRROR);
mPaint.setShader(mRadialGradient);
}
invalidate();
}
@Override
protected void onDraw(final Canvas canvas) {
super.onDraw(canvas);
if (isInEditMode()) {
return;
}
canvas.save(Canvas.CLIP_SAVE_FLAG);
mPath.reset();
mPath.addCircle(mDownX, mDownY, mRadius, Path.Direction.CW);
canvas.clipPath(mPath);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
canvas.restore();
canvas.drawCircle(mDownX, mDownY, mRadius, mPaint);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/ShadowProperty.java
================================================
package io.virtualapp.widgets;
public class ShadowProperty {
public static final int ALL = 0x1111;
public static final int LEFT = 0x0001;
public static final int TOP = 0x0010;
public static final int RIGHT = 0x0100;
public static final int BOTTOM = 0x1000;
/**
* 阴影颜色
*/
private int shadowColor;
/**
* 阴影半径
*/
private int shadowRadius;
/**
* 阴影x偏移
*/
private int shadowDx;
/**
* 阴影y偏移
*/
private int shadowDy;
/**
* 阴影边
*/
private int shadowSide = ALL;
public int getShadowSide() {
return shadowSide;
}
public ShadowProperty setShadowSide(int shadowSide) {
this.shadowSide = shadowSide;
return this;
}
public int getShadowOffset() {
return getShadowOffsetHalf() * 2;
}
public int getShadowOffsetHalf() {
return 0 >= shadowRadius ? 0 : Math.max(shadowDx, shadowDy) + shadowRadius;
}
public int getShadowColor() {
return shadowColor;
}
public ShadowProperty setShadowColor(int shadowColor) {
this.shadowColor = shadowColor;
return this;
}
public int getShadowRadius() {
return shadowRadius;
}
public ShadowProperty setShadowRadius(int shadowRadius) {
this.shadowRadius = shadowRadius;
return this;
}
public int getShadowDx() {
return shadowDx;
}
public ShadowProperty setShadowDx(int shadowDx) {
this.shadowDx = shadowDx;
return this;
}
public int getShadowDy() {
return shadowDy;
}
public ShadowProperty setShadowDy(int shadowDy) {
this.shadowDy = shadowDy;
return this;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/ShadowViewDrawable.java
================================================
package io.virtualapp.widgets;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
public class ShadowViewDrawable extends Drawable {
private Paint paint;
private RectF bounds = new RectF();
private int width;
private int height;
private ShadowProperty shadowProperty;
private int shadowOffset;
private RectF drawRect;
private float rx;
private float ry;
private PorterDuffXfermode srcOut = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT);
public ShadowViewDrawable(ShadowProperty shadowProperty, int color, float rx, float ry) {
this.shadowProperty = shadowProperty;
shadowOffset = this.shadowProperty.getShadowOffset();
this.rx = rx;
this.ry = ry;
paint = new Paint();
paint.setAntiAlias(true);
/**
* 解决旋转时的锯齿问题
*/
paint.setFilterBitmap(true);
paint.setDither(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(color);
/**
* 设置阴影
*/
paint.setShadowLayer(shadowProperty.getShadowRadius(), shadowProperty.getShadowDx(), shadowProperty.getShadowDy(), shadowProperty.getShadowColor());
drawRect = new RectF();
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
if (bounds.right - bounds.left > 0 && bounds.bottom - bounds.top > 0) {
this.bounds.left = bounds.left;
this.bounds.right = bounds.right;
this.bounds.top = bounds.top;
this.bounds.bottom = bounds.bottom;
width = (int) (this.bounds.right - this.bounds.left);
height = (int) (this.bounds.bottom - this.bounds.top);
int shadowSide = shadowProperty.getShadowSide();
int left = (shadowSide & ShadowProperty.LEFT) == ShadowProperty.LEFT ? shadowOffset : 0;
int top = (shadowSide & ShadowProperty.TOP) == ShadowProperty.TOP ? shadowOffset : 0;
int right = width - ((shadowSide & ShadowProperty.RIGHT) == ShadowProperty.RIGHT ? shadowOffset : 0);
int bottom = height - ((shadowSide & ShadowProperty.BOTTOM) == ShadowProperty.BOTTOM ? shadowOffset : 0);
drawRect = new RectF(left, top, right, bottom);
invalidateSelf();
}
}
@Override
public void draw(@NonNull Canvas canvas) {
paint.setXfermode(null);
canvas.drawRoundRect(
drawRect,
rx, ry,
paint
);
paint.setXfermode(srcOut);
canvas.drawRoundRect(drawRect, rx, ry, paint);
}
public ShadowViewDrawable setColor(int color) {
paint.setColor(color);
return this;
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(ColorFilter cf) {
}
@Override
public int getOpacity() {
return PixelFormat.UNKNOWN;
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/Shimmer.java
================================================
package io.virtualapp.widgets;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.os.Build;
import android.view.View;
public class Shimmer {
public static final int ANIMATION_DIRECTION_LTR = 0;
public static final int ANIMATION_DIRECTION_RTL = 1;
private static final int DEFAULT_REPEAT_COUNT = ValueAnimator.INFINITE;
private static final long DEFAULT_DURATION = 1000;
private static final long DEFAULT_START_DELAY = 0;
private static final int DEFAULT_DIRECTION = ANIMATION_DIRECTION_LTR;
private int repeatCount;
private long duration;
private long startDelay;
private int direction;
private Animator.AnimatorListener animatorListener;
private ObjectAnimator animator;
public Shimmer() {
repeatCount = DEFAULT_REPEAT_COUNT;
duration = DEFAULT_DURATION;
startDelay = DEFAULT_START_DELAY;
direction = DEFAULT_DIRECTION;
}
public int getRepeatCount() {
return repeatCount;
}
public Shimmer setRepeatCount(int repeatCount) {
this.repeatCount = repeatCount;
return this;
}
public long getDuration() {
return duration;
}
public Shimmer setDuration(long duration) {
this.duration = duration;
return this;
}
public long getStartDelay() {
return startDelay;
}
public Shimmer setStartDelay(long startDelay) {
this.startDelay = startDelay;
return this;
}
public int getDirection() {
return direction;
}
public Shimmer setDirection(int direction) {
if (direction != ANIMATION_DIRECTION_LTR && direction != ANIMATION_DIRECTION_RTL) {
throw new IllegalArgumentException("The animation direction must be either ANIMATION_DIRECTION_LTR or ANIMATION_DIRECTION_RTL");
}
this.direction = direction;
return this;
}
public Animator.AnimatorListener getAnimatorListener() {
return animatorListener;
}
public Shimmer setAnimatorListener(Animator.AnimatorListener animatorListener) {
this.animatorListener = animatorListener;
return this;
}
public void start(final V shimmerView) {
if (isAnimating()) {
return;
}
final Runnable animate = new Runnable() {
@Override
public void run() {
shimmerView.setShimmering(true);
float fromX = 0;
float toX = shimmerView.getWidth();
if (direction == ANIMATION_DIRECTION_RTL) {
fromX = shimmerView.getWidth();
toX = 0;
}
animator = ObjectAnimator.ofFloat(shimmerView, "gradientX", fromX, toX);
animator.setRepeatCount(repeatCount);
animator.setDuration(duration);
animator.setStartDelay(startDelay);
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
shimmerView.setShimmering(false);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
shimmerView.postInvalidate();
} else {
shimmerView.postInvalidateOnAnimation();
}
animator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
if (animatorListener != null) {
animator.addListener(animatorListener);
}
animator.start();
}
};
if (!shimmerView.isSetUp()) {
shimmerView.setAnimationSetupCallback(new ShimmerViewHelper.AnimationSetupCallback() {
@Override
public void onSetupAnimation(final View target) {
animate.run();
}
});
} else {
animate.run();
}
}
public void cancel() {
if (animator != null) {
animator.cancel();
}
}
public boolean isAnimating() {
return animator != null && animator.isRunning();
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/ShimmerViewBase.java
================================================
package io.virtualapp.widgets;
public interface ShimmerViewBase {
float getGradientX();
void setGradientX(float gradientX);
boolean isShimmering();
void setShimmering(boolean isShimmering);
boolean isSetUp();
void setAnimationSetupCallback(ShimmerViewHelper.AnimationSetupCallback callback);
int getPrimaryColor();
void setPrimaryColor(int primaryColor);
int getReflectionColor();
void setReflectionColor(int reflectionColor);
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/ShimmerViewHelper.java
================================================
package io.virtualapp.widgets;
import android.content.res.TypedArray;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;
import io.virtualapp.R;
public class ShimmerViewHelper {
private static final int DEFAULT_REFLECTION_COLOR = 0xFFFFFFFF;
private View view;
private Paint paint;
// center position of the gradient
private float gradientX;
// shader applied on the text view
// only null until the first global layout
private LinearGradient linearGradient;
// shader's local matrix
// never null
private Matrix linearGradientMatrix;
private int primaryColor;
// shimmer reflection color
private int reflectionColor;
// true when animating
private boolean isShimmering;
// true after first global layout
private boolean isSetUp;
// callback called after first global layout
private AnimationSetupCallback callback;
public ShimmerViewHelper(View view, Paint paint, AttributeSet attributeSet) {
this.view = view;
this.paint = paint;
init(attributeSet);
}
public float getGradientX() {
return gradientX;
}
public void setGradientX(float gradientX) {
this.gradientX = gradientX;
view.invalidate();
}
public boolean isShimmering() {
return isShimmering;
}
public void setShimmering(boolean isShimmering) {
this.isShimmering = isShimmering;
}
public boolean isSetUp() {
return isSetUp;
}
public void setAnimationSetupCallback(AnimationSetupCallback callback) {
this.callback = callback;
}
public int getPrimaryColor() {
return primaryColor;
}
public void setPrimaryColor(int primaryColor) {
this.primaryColor = primaryColor;
if (isSetUp) {
resetLinearGradient();
}
}
public int getReflectionColor() {
return reflectionColor;
}
public void setReflectionColor(int reflectionColor) {
this.reflectionColor = reflectionColor;
if (isSetUp) {
resetLinearGradient();
}
}
private void init(AttributeSet attributeSet) {
reflectionColor = DEFAULT_REFLECTION_COLOR;
if (attributeSet != null) {
TypedArray a = view.getContext().obtainStyledAttributes(attributeSet, R.styleable.ShimmerView, 0, 0);
if (a != null) {
try {
reflectionColor = a.getColor(R.styleable.ShimmerView_reflectionColor, DEFAULT_REFLECTION_COLOR);
} catch (Exception e) {
android.util.Log.e("ShimmerTextView", "Error while creating the view:", e);
} finally {
a.recycle();
}
}
}
linearGradientMatrix = new Matrix();
}
private void resetLinearGradient() {
// our gradient is a simple linear gradient from textColor to reflectionColor. its axis is at the center
// when it's outside of the view, the outer color (textColor) will be repeated (Shader.TileMode.CLAMP)
// initially, the linear gradient is positioned on the left side of the view
linearGradient = new LinearGradient(-view.getWidth(), 0, 0, 0,
new int[]{
primaryColor,
reflectionColor,
primaryColor,
},
new float[]{
0,
0.5f,
1
},
Shader.TileMode.CLAMP
);
paint.setShader(linearGradient);
}
protected void onSizeChanged() {
resetLinearGradient();
if (!isSetUp) {
isSetUp = true;
if (callback != null) {
callback.onSetupAnimation(view);
}
}
}
/**
* content of the wrapping view's onDraw(Canvas)
* MUST BE CALLED BEFORE SUPER STATEMENT
*/
public void onDraw() {
// only draw the shader gradient over the text while animating
if (isShimmering) {
// first onDraw() when shimmering
if (paint.getShader() == null) {
paint.setShader(linearGradient);
}
// translate the shader local matrix
linearGradientMatrix.setTranslate(2 * gradientX, 0);
// this is required in order to invalidate the shader's position
linearGradient.setLocalMatrix(linearGradientMatrix);
} else {
// we're not animating, remove the shader from the paint
paint.setShader(null);
}
}
public interface AnimationSetupCallback {
void onSetupAnimation(View target);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/TwoGearsView.java
================================================
package io.virtualapp.widgets;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
public class TwoGearsView extends BaseView {
ValueAnimator valueAnimator = null;
float mAnimatedValue = 0f;
float hypotenuse = 0f;
float smallRingCenterX = 0f;
float smallRingCenterY = 0f;
float bigRingCenterX = 0f;
float bigRingCenterY = 0f;
private float mWidth = 0f;
private Paint mPaint, mPaintAxle;
private Paint mPaintRing;
private float mPadding = 0f;
private float mWheelLength;
private int mWheelSmallSpace = 10;
private int mWheelBigSpace = 8;
public TwoGearsView(Context context) {
super(context);
}
public TwoGearsView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TwoGearsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getMeasuredWidth() > getHeight())
mWidth = getMeasuredHeight();
else
mWidth = getMeasuredWidth();
}
private void drawSmallRing(Canvas canvas) {
hypotenuse = (float) (mWidth * Math.sqrt(2));
smallRingCenterX = (float) ((hypotenuse / 6.f) * Math.cos(45 * Math.PI / 180f));
smallRingCenterY = (float) ((hypotenuse / 6.f) * Math.sin(45 * Math.PI / 180f));
mPaintRing.setStrokeWidth(dip2px(1.0f));
canvas.drawCircle(mPadding + smallRingCenterX, smallRingCenterY + mPadding, smallRingCenterX, mPaintRing);
mPaintRing.setStrokeWidth(dip2px(1.5f));
canvas.drawCircle(mPadding + smallRingCenterX, smallRingCenterY + mPadding, smallRingCenterX / 2, mPaintRing);
}
private void drawSmallGear(Canvas canvas) {
mPaint.setStrokeWidth(dip2px(1));
for (int i = 0; i < 360; i = i + mWheelSmallSpace) {
int angle = (int) (mAnimatedValue * mWheelSmallSpace + i);
float x3 = (float) ((smallRingCenterX) * Math.cos(angle * Math.PI / 180f));
float y3 = (float) ((smallRingCenterY) * Math.sin(angle * Math.PI / 180f));
float x4 = (float) ((smallRingCenterX + mWheelLength) * Math.cos(angle * Math.PI / 180f));
float y4 = (float) ((smallRingCenterY + mWheelLength) * Math.sin(angle * Math.PI / 180f));
canvas.drawLine(mPadding + smallRingCenterX - x4,
smallRingCenterY + mPadding - y4,
smallRingCenterX + mPadding - x3,
smallRingCenterY + mPadding - y3,
mPaint);
}
}
private void drawBigGear(Canvas canvas) {
bigRingCenterX = (float) ((hypotenuse / 2.f) * Math.cos(45 * Math.PI / 180f));
bigRingCenterY = (float) ((hypotenuse / 2.f) * Math.sin(45 * Math.PI / 180f));
float strokeWidth = dip2px(1.5f) / 4;
mPaint.setStrokeWidth(dip2px(1.5f));
for (int i = 0; i < 360; i = i + mWheelBigSpace) {
int angle = (int) (360 - (mAnimatedValue * mWheelBigSpace + i));
float x3 = (float) ((bigRingCenterX - smallRingCenterX) * Math.cos(angle * Math.PI / 180f));
float y3 = (float) ((bigRingCenterY - smallRingCenterY) * Math.sin(angle * Math.PI / 180f));
float x4 = (float) ((bigRingCenterX - smallRingCenterX + mWheelLength) * Math.cos(angle * Math.PI / 180f));
float y4 = (float) ((bigRingCenterY - smallRingCenterY + mWheelLength) * Math.sin(angle * Math.PI / 180f));
canvas.drawLine(bigRingCenterX + mPadding - x4 + mWheelLength * 2 + strokeWidth,
bigRingCenterY + mPadding - y4 + mWheelLength * 2 + strokeWidth,
bigRingCenterX + mPadding - x3 + mWheelLength * 2 + strokeWidth,
bigRingCenterY + mPadding - y3 + mWheelLength * 2 + strokeWidth,
mPaint);
}
}
private void drawBigRing(Canvas canvas) {
float strokeWidth = dip2px(1.5f) / 4;
mPaintRing.setStrokeWidth(dip2px(1.5f));
canvas.drawCircle(bigRingCenterX + mPadding + mWheelLength * 2 + strokeWidth,
bigRingCenterY + mPadding + mWheelLength * 2 + strokeWidth,
bigRingCenterX - smallRingCenterX - strokeWidth, mPaintRing);
mPaintRing.setStrokeWidth(dip2px(1.5f));
canvas.drawCircle(bigRingCenterX + mPadding + mWheelLength * 2 + strokeWidth,
bigRingCenterY + mPadding + mWheelLength * 2 + strokeWidth,
(bigRingCenterX - smallRingCenterX) / 2 - strokeWidth, mPaintRing);
}
private void drawAxle(Canvas canvas) {
for (int i = 0; i < 3; i++) {
float x3 = (float) ((smallRingCenterX) * Math.cos(i * (360 / 3) * Math.PI / 180f));
float y3 = (float) ((smallRingCenterY) * Math.sin(i * (360 / 3) * Math.PI / 180f));
canvas.drawLine(mPadding + smallRingCenterX,
mPadding + smallRingCenterY,
mPadding + smallRingCenterX - x3,
mPadding + smallRingCenterY - y3, mPaintAxle);
}
for (int i = 0; i < 3; i++) {
float x3 = (float) ((bigRingCenterX - smallRingCenterX) * Math.cos(i * (360 / 3) * Math.PI / 180f));
float y3 = (float) ((bigRingCenterY - smallRingCenterY) * Math.sin(i * (360 / 3) * Math.PI / 180f));
canvas.drawLine(bigRingCenterX + mPadding + mWheelLength * 2,
bigRingCenterY + mPadding + mWheelLength * 2,
bigRingCenterX + mPadding + mWheelLength * 2 - x3,
bigRingCenterY + mPadding + mWheelLength * 2 - y3,
mPaintAxle);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPadding = dip2px(5);
canvas.save();
canvas.rotate(180, mWidth / 2, mWidth / 2);
drawSmallRing(canvas);
drawSmallGear(canvas);
drawBigGear(canvas);
drawBigRing(canvas);
drawAxle(canvas);
canvas.restore();
}
private void initPaint() {
mPaintRing = new Paint();
mPaintRing.setAntiAlias(true);
mPaintRing.setStyle(Paint.Style.STROKE);
mPaintRing.setColor(Color.WHITE);
mPaintRing.setStrokeWidth(dip2px(1.5f));
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.WHITE);
mPaint.setStrokeWidth(dip2px(1));
mPaintAxle = new Paint();
mPaintAxle.setAntiAlias(true);
mPaintAxle.setStyle(Paint.Style.FILL);
mPaintAxle.setColor(Color.WHITE);
mPaintAxle.setStrokeWidth(dip2px(1.5f));
mWheelLength = dip2px(2f);
}
public void setViewColor(int color) {
mPaint.setColor(color);
mPaintAxle.setColor(color);
mPaintRing.setColor(color);
postInvalidate();
}
@Override
protected void InitPaint() {
initPaint();
}
@Override
protected void OnAnimationUpdate(ValueAnimator valueAnimator) {
mAnimatedValue = (float) valueAnimator.getAnimatedValue();
postInvalidate();
}
@Override
protected void OnAnimationRepeat(Animator animation) {
}
@Override
protected int OnStopAnim() {
postInvalidate();
return 1;
}
@Override
protected int SetAnimRepeatMode() {
return ValueAnimator.RESTART;
}
@Override
protected void AnimIsRunning() {
}
@Override
protected int SetAnimRepeatCount() {
return ValueAnimator.INFINITE;
}
private int dip2px(float dpValue) {
final float scale = getContext().getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
}
================================================
FILE: app/src/main/java/io/virtualapp/widgets/ViewHelper.java
================================================
package io.virtualapp.widgets;
import io.virtualapp.VApp;
/**
* @author Lody
*/
public class ViewHelper {
public static int dip2px(float dpValue) {
final float scale = VApp.getApp().getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
}
================================================
FILE: app/src/main/res/drawable/blue_circle.xml
================================================
================================================
FILE: app/src/main/res/drawable/fab_bg.xml
================================================
================================================
FILE: app/src/main/res/drawable/home_bg.xml
================================================
================================================
FILE: app/src/main/res/drawable/icon_bg.xml
================================================
================================================
FILE: app/src/main/res/drawable/sel_clone_app_btn.xml
================================================
================================================
FILE: app/src/main/res/drawable/sel_guide_btn.xml
================================================
================================================
FILE: app/src/main/res/drawable/shape_clone_app_btn.xml
================================================
================================================
FILE: app/src/main/res/drawable/shape_clone_app_btn_pressed.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_clone_app.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_home.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_install.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_loading.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_splash.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_users.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_list_app.xml
================================================
================================================
FILE: app/src/main/res/layout/item_app.xml
================================================
================================================
FILE: app/src/main/res/layout/item_clone_app.xml
================================================
================================================
FILE: app/src/main/res/layout/item_launcher_app.xml
================================================
================================================
FILE: app/src/main/res/layout/item_user.xml
================================================
================================================
FILE: app/src/main/res/menu/user_menu.xml
================================================
================================================
FILE: app/src/main/res/values/attrs.xml
================================================
================================================
FILE: app/src/main/res/values/colors.xml
================================================
#607191#2A364C#607191#607191#F0DEB7#2C3B4E#314155#324257#2a3646#33cccc#ff9640#67e667#df38b1#ff4040@color/holo_blue_dark@color/holo_yellow_dark@color/holo_green_dark@color/holo_purple_dark@color/holo_red_dark#00000000#55000000#00FFFFFF
================================================
FILE: app/src/main/res/values/dimens.xml
================================================
16dp16dp60dp5dp30dp8dp10dp80dp5dp16dp24dp0.5dp60dp56dp
================================================
FILE: app/src/main/res/values/ids.xml
================================================
================================================
FILE: app/src/main/res/values/integers.xml
================================================
-2
================================================
FILE: app/src/main/res/values/strings.xml
================================================
VirtualAppPlease wait…DesktopAdd AppOpening the app…DeleteCreate shortcutNew UserEnableSaveSave success!ManufacturerBrandDeviceFake Device InfoWifi StatusDevice InfoAboutClone AppsExternal StorageInstall (%d)No more then 9 apps can be chosen at a time!
================================================
FILE: app/src/main/res/values/styles.xml
================================================
================================================
FILE: app/src/main/res/values-zh-rCN/strings.xml
================================================
VirtualApp桌面添加App正在打开App,请稍等…删除创建快捷方式新的用户开启保存保存成功!制造商请稍后…品牌机型伪造设备信息Wifi状态配置设备信息关于克隆App外置存储安装 (%d)不能一次性安装超过9个App!
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'me.tatarka:gradle-retrolambda:3.6.0'
classpath 'com.android.tools.build:gradle-experimental:0.8.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
maven {
url "https://jitpack.io"
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Sun Jan 15 17:29:32 CST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useDeprecatedNdk=true
================================================
FILE: gradlew
================================================
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: lib/.gitignore
================================================
/build
.externalNativeBuild/
obj/
================================================
FILE: lib/build.gradle
================================================
apply plugin: 'com.android.library'
android {
compileSdkVersion 24
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 14
targetSdkVersion 22
versionCode 1
versionName "1.0"
externalNativeBuild {
ndkBuild {
abiFilters "armeabi", "armeabi-v7a", "x86"
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
ndkBuild {
path file("src/main/jni/Android.mk")
}
}
lintOptions {
//IJobService need NewApi
warning 'NewApi','OnClick'
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
// compile 'net.lingala.zip4j:zip4j:1.3.2'
compile files('src/main/libs/android-art-interpret-3.0.0.jar')
compile files('src/main/libs/dalvik_hack-3.0.0.5.jar')
}
================================================
FILE: lib/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/lody/Desktop/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
================================================
FILE: lib/src/main/AndroidManifest.xml
================================================
================================================
FILE: lib/src/main/aidl/android/accounts/IAccountAuthenticator.aidl
================================================
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.accounts;
import android.accounts.IAccountAuthenticatorResponse;
import android.accounts.Account;
import android.os.Bundle;
/**
* Service that allows the interaction with an authentication server.
* @hide
*/
interface IAccountAuthenticator {
/**
* prompts the user for account information and adds the result to the IAccountManager
*/
void addAccount(in IAccountAuthenticatorResponse response, String accountType,
String authTokenType, in String[] requiredFeatures, in Bundle options);
/**
* prompts the user for the credentials of the account
*/
void confirmCredentials(in IAccountAuthenticatorResponse response, in Account account,
in Bundle options);
/**
* gets the password by either prompting the user or querying the IAccountManager
*/
void getAuthToken(in IAccountAuthenticatorResponse response, in Account account,
String authTokenType, in Bundle options);
/**
* Gets the user-visible label of the given authtoken type.
*/
void getAuthTokenLabel(in IAccountAuthenticatorResponse response, String authTokenType);
/**
* prompts the user for a new password and writes it to the IAccountManager
*/
void updateCredentials(in IAccountAuthenticatorResponse response, in Account account,
String authTokenType, in Bundle options);
/**
* launches an activity that lets the user edit and set the properties for an authenticator
*/
void editProperties(in IAccountAuthenticatorResponse response, String accountType);
/**
* returns a Bundle where the boolean value BOOLEAN_RESULT_KEY is set if the account has the
* specified features
*/
void hasFeatures(in IAccountAuthenticatorResponse response, in Account account,
in String[] features);
/**
* Gets whether or not the account is allowed to be removed.
*/
void getAccountRemovalAllowed(in IAccountAuthenticatorResponse response, in Account account);
/**
* Returns a Bundle containing the required credentials to copy the account across users.
*/
void getAccountCredentialsForCloning(in IAccountAuthenticatorResponse response,
in Account account);
/**
* Uses the Bundle containing credentials from another instance of the authenticator to create
* a copy of the account on this user.
*/
void addAccountFromCredentials(in IAccountAuthenticatorResponse response, in Account account,
in Bundle accountCredentials);
}
================================================
FILE: lib/src/main/aidl/android/accounts/IAccountAuthenticatorResponse.aidl
================================================
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.accounts;
import android.os.Bundle;
/**
* The interface used to return responses from an {@link IAccountAuthenticator}
*/
interface IAccountAuthenticatorResponse {
void onResult(in Bundle value);
void onRequestContinued();
void onError(int errorCode, String errorMessage);
}
================================================
FILE: lib/src/main/aidl/android/accounts/IAccountManagerResponse.aidl
================================================
package android.accounts;
import android.os.Bundle;
/**
* The interface used to return responses for asynchronous calls to the {@link IAccountManager}
*/
interface IAccountManagerResponse {
void onResult(in Bundle value);
void onError(int errorCode, String errorMessage);
}
================================================
FILE: lib/src/main/aidl/android/app/IActivityManager/ContentProviderHolder.aidl
================================================
// ContentProviderHolder.aidl
package android.app.IActivityManager;
parcelable ContentProviderHolder;
================================================
FILE: lib/src/main/aidl/android/app/IServiceConnection.aidl
================================================
/* //device/java/android/android/app/IServiceConnection.aidl
**
** Copyright 2007, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
package android.app;
import android.content.ComponentName;
/** @hide */
interface IServiceConnection {
void connected(in ComponentName name, IBinder service);
}
================================================
FILE: lib/src/main/aidl/android/app/IStopUserCallback.aidl
================================================
/*
** Copyright 2012, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
package android.app;
/**
* Callback to find out when we have finished stopping a user.
* {@hide}
*/
interface IStopUserCallback
{
void userStopped(int userId);
void userStopAborted(int userId);
}
================================================
FILE: lib/src/main/aidl/android/app/job/IJobCallback.aidl
================================================
/**
* Copyright 2014, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app.job;
/**
* The server side of the JobScheduler IPC protocols. The app-side implementation
* invokes on this interface to indicate completion of the (asynchronous) instructions
* issued by the server.
*
* In all cases, the 'who' parameter is the caller's service binder, used to track
* which Job Service instance is reporting.
*
*/
interface IJobCallback {
/**
* Immediate callback to the system after sending a start signal, used to quickly detect ANR.
*
* @param jobId Unique integer used to identify this job.
* @param ongoing True to indicate that the client is processing the job. False if the job is
* complete
*/
void acknowledgeStartMessage(int jobId, boolean ongoing);
/**
* Immediate callback to the system after sending a stop signal, used to quickly detect ANR.
*
* @param jobId Unique integer used to identify this job.
* @param reschedule Whether or not to reschedule this job.
*/
void acknowledgeStopMessage(int jobId, boolean reschedule);
/*
* Tell the job manager that the client is done with its execution, so that it can go on to
* the next one and stop attributing wakelock time to us etc.
*
* @param jobId Unique integer used to identify this job.
* @param reschedule Whether or not to reschedule this job.
*/
void jobFinished(int jobId, boolean reschedule);
}
================================================
FILE: lib/src/main/aidl/android/app/job/IJobService.aidl
================================================
package android.app.job;
import android.app.job.JobParameters;
/**
* Interface that the framework uses to communicate with application code that implements a
* JobService. End user code does not implement this interface directly; instead, the app's
* service implementation will extend android.app.job.JobService.
*/
interface IJobService {
/** Begin execution of application's job. */
void startJob(in JobParameters jobParams);
/** Stop execution of application's job. */
void stopJob(in JobParameters jobParams);
}
================================================
FILE: lib/src/main/aidl/android/content/IIntentReceiver.aidl
================================================
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content;
import android.content.Intent;
import android.os.Bundle;
/**
* System private API for dispatching intent broadcasts. This is given to the
* activity manager as part of registering for an intent broadcasts, and is
* called when it receives intents.
*
*/
interface IIntentReceiver {
void performReceive(in Intent intent, int resultCode, String data,
in Bundle extras, boolean ordered, boolean sticky, int sendingUser);
}
================================================
FILE: lib/src/main/aidl/android/content/ISyncAdapter.aidl
================================================
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content;
import android.accounts.Account;
import android.os.Bundle;
import android.content.ISyncContext;
/**
* Interface used to control the sync activity on a SyncAdapter
*/
interface ISyncAdapter {
/**
* Initiate a sync for this account. SyncAdapter-specific parameters may
* be specified in extras, which is guaranteed to not be null.
*
* @param syncContext the ISyncContext used to indicate the progress of the sync. When
* the sync is finished (successfully or not) ISyncContext.onFinished() must be called.
* @param authority the authority that should be synced
* @param account the account that should be synced
* @param extras SyncAdapter-specific parameters
*/
void startSync(ISyncContext syncContext, String authority,
in Account account, in Bundle extras);
/**
* Cancel the most recently initiated sync. Due to race conditions, this may arrive
* after the ISyncContext.onFinished() for that sync was called.
* @param syncContext the ISyncContext that was passed to {@link #startSync}
*/
void cancelSync(ISyncContext syncContext);
/**
* Initialize the SyncAdapter for this account and authority.
*
* @param account the account that should be synced
* @param authority the authority that should be synced
*/
void initialize(in Account account, String authority);
}
================================================
FILE: lib/src/main/aidl/android/content/ISyncContext.aidl
================================================
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content;
import android.content.SyncResult;
/**
* Interface used by the SyncAdapter to indicate its progress.
* @hide
*/
interface ISyncContext {
/**
* Call to indicate that the SyncAdapter is making progress. E.g., if this SyncAdapter
* downloads or sends records to/from the server, this may be called after each record
* is downloaded or uploaded.
*/
void sendHeartbeat();
/**
* Signal that the corresponding sync session is completed.
* @param result information about this sync session
*/
void onFinished(in SyncResult result);
}
================================================
FILE: lib/src/main/aidl/android/content/ISyncStatusObserver.aidl
================================================
package android.content;
interface ISyncStatusObserver {
void onStatusChanged(int which);
}
================================================
FILE: lib/src/main/aidl/android/content/pm/IPackageDataObserver.aidl
================================================
package android.content.pm;
/**
* API for package data change related callbacks from the Package Manager.
* Some usage scenarios include deletion of cache directory, generate
* statistics related to code, data, cache usage(TODO)
*/
interface IPackageDataObserver {
void onRemoveCompleted(in String packageName, boolean succeeded);
}
================================================
FILE: lib/src/main/aidl/android/content/pm/IPackageDeleteObserver2.aidl
================================================
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content.pm;
import android.content.Intent;
interface IPackageDeleteObserver2 {
void onUserActionRequired(in Intent intent);
void onPackageDeleted(String packageName, int returnCode, String msg);
}
================================================
FILE: lib/src/main/aidl/android/content/pm/IPackageInstallObserver.aidl
================================================
package android.content.pm;
/**
* API for installation callbacks from the Package Manager.
*/
interface IPackageInstallObserver {
void packageInstalled(in String packageName, int returnCode);
}
================================================
FILE: lib/src/main/aidl/android/content/pm/IPackageInstallObserver2.aidl
================================================
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content.pm;
import android.content.Intent;
import android.os.Bundle;
/**
* API for installation callbacks from the Package Manager. In certain result cases
* additional information will be provided.
*/
interface IPackageInstallObserver2 {
void onUserActionRequired(in Intent intent);
/**
* The install operation has completed. {@code returnCode} holds a numeric code
* indicating success or failure. In certain cases the {@code extras} Bundle will
* contain additional details:
*
*
*
*
INSTALL_FAILED_DUPLICATE_PERMISSION
*
Two strings are provided in the extras bundle: EXTRA_EXISTING_PERMISSION
* is the name of the permission that the app is attempting to define, and
* EXTRA_EXISTING_PACKAGE is the package name of the app which has already
* defined the permission.
*
*
*/
void onPackageInstalled(String basePackageName, int returnCode, String msg, in Bundle extras);
}
================================================
FILE: lib/src/main/aidl/android/content/pm/IPackageInstallerCallback.aidl
================================================
package android.content.pm;
interface IPackageInstallerCallback {
void onSessionCreated(int sessionId);
void onSessionBadgingChanged(int sessionId);
void onSessionActiveChanged(int sessionId, boolean active);
void onSessionProgressChanged(int sessionId, float progress);
void onSessionFinished(int sessionId, boolean success);
}
================================================
FILE: lib/src/main/aidl/android/content/pm/IPackageInstallerSession.aidl
================================================
package android.content.pm;
import android.content.pm.IPackageInstallObserver2;
import android.content.IntentSender;
import android.os.ParcelFileDescriptor;
interface IPackageInstallerSession {
void setClientProgress(float progress);
void addClientProgress(float progress);
String[] getNames();
ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes);
ParcelFileDescriptor openRead(String name);
void removeSplit(String splitName);
void close();
void commit(in IntentSender statusReceiver);
void abandon();
}
================================================
FILE: lib/src/main/aidl/android/net/IConnectivityManager.aidl
================================================
package android.net;
import android.net.NetworkInfo;
import android.net.LinkProperties;
interface IConnectivityManager {
NetworkInfo getActiveNetworkInfo();
NetworkInfo getActiveNetworkInfoForUid(int uid, boolean ignoreBlocked);
NetworkInfo getNetworkInfo(int networkType);
NetworkInfo[] getAllNetworkInfo();
boolean isActiveNetworkMetered();
boolean requestRouteToHostAddress(int networkType, int address);
LinkProperties getActiveLinkProperties();
LinkProperties getLinkProperties(int networkType);
}
================================================
FILE: lib/src/main/aidl/android/net/wifi/IWifiScanner.aidl
================================================
package android.net.wifi;
import android.os.Messenger;
import android.os.Bundle;
interface IWifiScanner
{
Messenger getMessenger();
Bundle getAvailableChannels(int band);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/client/IVClient.aidl
================================================
// IVClient.aidl
package com.lody.virtual.client;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.ProviderInfo;
import com.lody.virtual.remote.PendingResultData;
interface IVClient {
void scheduleReceiver(in String processName,in ComponentName component, in Intent intent, in PendingResultData resultData);
void scheduleNewIntent(in String creator, in IBinder token, in Intent intent);
void finishActivity(in IBinder token);
IBinder createProxyService(in ComponentName component, in IBinder binder);
IBinder acquireProviderClient(in ProviderInfo info);
IBinder getAppThread();
IBinder getToken();
String getDebugInfo();
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/os/VUserInfo.aidl
================================================
// VUserInfo.aidl
package com.lody.virtual.os;
parcelable VUserInfo;
================================================
FILE: lib/src/main/aidl/com/lody/virtual/remote/AppTaskInfo.aidl
================================================
// AppTaskInfo.aidl
package com.lody.virtual.remote;
parcelable AppTaskInfo;
================================================
FILE: lib/src/main/aidl/com/lody/virtual/remote/InstallResult.aidl
================================================
// InstallResult.aidl
package com.lody.virtual.remote;
parcelable InstallResult;
================================================
FILE: lib/src/main/aidl/com/lody/virtual/remote/InstalledAppInfo.aidl
================================================
// AppSetting.aidl
package com.lody.virtual.remote;
parcelable InstalledAppInfo;
================================================
FILE: lib/src/main/aidl/com/lody/virtual/remote/PendingIntentData.aidl
================================================
// PendingIntentData.aidl
package com.lody.virtual.remote;
parcelable PendingIntentData;
================================================
FILE: lib/src/main/aidl/com/lody/virtual/remote/PendingResultData.aidl
================================================
// PendingResultData.aidl
package com.lody.virtual.remote;
parcelable PendingResultData;
================================================
FILE: lib/src/main/aidl/com/lody/virtual/remote/Problem.aidl
================================================
// Problem.aidl
package com.lody.virtual.remote;
parcelable Problem;
================================================
FILE: lib/src/main/aidl/com/lody/virtual/remote/ReceiverInfo.aidl
================================================
// ReceiverInfo.aidl
package com.lody.virtual.remote;
parcelable ReceiverInfo;
================================================
FILE: lib/src/main/aidl/com/lody/virtual/remote/VDeviceInfo.aidl
================================================
// VDeviceInfo.aidl
package com.lody.virtual.remote;
parcelable VDeviceInfo;
================================================
FILE: lib/src/main/aidl/com/lody/virtual/remote/VParceledListSlice.aidl
================================================
// VParceledListSlice.aidl
package com.lody.virtual.remote;
parcelable VParceledListSlice;
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/IAccountManager.aidl
================================================
package com.lody.virtual.server;
import android.accounts.IAccountManagerResponse;
import android.accounts.Account;
import android.accounts.AuthenticatorDescription;
import android.os.Bundle;
/**
* Central application service that provides account management.
* @hide
*/
interface IAccountManager {
AuthenticatorDescription[] getAuthenticatorTypes(int userId);
void getAccountsByFeatures(int userId, in IAccountManagerResponse response, in String type, in String[] features);
String getPreviousName(int userId, in Account account);
Account[] getAccounts(int userId, in String type);
void getAuthToken(int userId, in IAccountManagerResponse response, in Account account, in String authTokenType, in boolean notifyOnAuthFailure, in boolean expectActivityLaunch, in Bundle loginOptions);
void setPassword(int userId, in Account account, in String password);
void setAuthToken(int userId, in Account account, in String authTokenType, in String authToken);
void setUserData(int userId, in Account account, in String key, in String value);
void hasFeatures(int userId, in IAccountManagerResponse response,
in Account account, in String[] features);
void updateCredentials(int userId, in IAccountManagerResponse response, in Account account,
in String authTokenType, in boolean expectActivityLaunch,
in Bundle loginOptions);
void editProperties(int userId, in IAccountManagerResponse response, in String accountType,
in boolean expectActivityLaunch);
void getAuthTokenLabel(int userId, in IAccountManagerResponse response, in String accountType,
in String authTokenType);
String getUserData(int userId, in Account account, in String key);
String getPassword(int userId, in Account account);
void confirmCredentials(int userId, in IAccountManagerResponse response, in Account account, in Bundle options, in boolean expectActivityLaunch);
void addAccount(int userId, in IAccountManagerResponse response, in String accountType,
in String authTokenType, in String[] requiredFeatures,
in boolean expectActivityLaunch, in Bundle optionsIn);
boolean addAccountExplicitly(int userId, in Account account, in String password, in Bundle extras);
boolean removeAccountExplicitly(int userId, in Account account);
void renameAccount(int userId, in IAccountManagerResponse response, in Account accountToRename, in String newName);
void removeAccount(in int userId, in IAccountManagerResponse response, in Account account,
in boolean expectActivityLaunch);
void clearPassword(int userId, in Account account);
boolean accountAuthenticated(int userId, in Account account);
void invalidateAuthToken(int userId, in String accountType, in String authToken);
String peekAuthToken(int userId, in Account account, in String authTokenType);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/IActivityManager.aidl
================================================
// IActivityManager.aidl
package com.lody.virtual.server;
import com.lody.virtual.remote.VParceledListSlice;
import com.lody.virtual.remote.AppTaskInfo;
import com.lody.virtual.remote.PendingIntentData;
import com.lody.virtual.remote.PendingResultData;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.app.Notification;
import android.app.IServiceConnection;
import android.app.IActivityManager.ContentProviderHolder;
import com.lody.virtual.server.interfaces.IProcessObserver;
interface IActivityManager {
int initProcess(in String packageName, in String processName, int userId);
int getFreeStubCount();
int getSystemPid();
int getUidByPid(int pid);
boolean isAppProcess(String processName);
boolean isAppRunning(String packageName, int userId);
boolean isAppPid(int pid);
String getAppProcessName(int pid);
List getProcessPkgList(int pid);
void killAllApps();
void killAppByPkg(String pkg, int userId);
void killApplicationProcess(String procName, int vuid);
void dump();
void registerProcessObserver(in IProcessObserver observer);
void unregisterProcessObserver(in IProcessObserver observer);
String getInitialPackage(int pid);
void handleApplicationCrash();
void appDoneExecuting();
int startActivities(in Intent[] intents, in String[] resolvedTypes, in IBinder token, in Bundle options, in int userId);
int startActivity(in Intent intent, in ActivityInfo info, in IBinder resultTo, in Bundle options, String resultWho, int requestCode, int userId);
void onActivityCreated(in ComponentName component, in ComponentName caller, in IBinder token, in Intent intent, in String affinity, int taskId, int launchMode, int flags);
void onActivityResumed(int userId, in IBinder token);
boolean onActivityDestroyed(int userId, in IBinder token);
ComponentName getActivityClassForToken(int userId, in IBinder token);
String getCallingPackage(int userId, in IBinder token);
ComponentName getCallingActivity(int userId, in IBinder token);
AppTaskInfo getTaskInfo(int taskId);
String getPackageForToken(int userId, in IBinder token);
boolean isVAServiceToken(in IBinder token);
ComponentName startService(in IBinder caller,in Intent service, String resolvedType, int userId);
int stopService(in IBinder caller, in Intent service, String resolvedType, int userId);
boolean stopServiceToken(in ComponentName className, in IBinder token, int startId, int userId);
void setServiceForeground(in ComponentName className, in IBinder token, int id,
in Notification notification, boolean removeNotification, int userId);
int bindService(in IBinder caller, in IBinder token, in Intent service,
String resolvedType, in IServiceConnection connection, int flags, int userId);
boolean unbindService(in IServiceConnection connection, int userId);
void unbindFinished(in IBinder token, in Intent service, in boolean doRebind, int userId);
void serviceDoneExecuting(in IBinder token, in int type, in int startId, in int res, int userId);
IBinder peekService(in Intent service, String resolvedType, int userId);
void publishService(in IBinder token, in Intent intent, in IBinder service, int userId);
VParceledListSlice getServices(int maxNum, int flags, int userId);
IBinder acquireProviderClient(int userId, in ProviderInfo info);
PendingIntentData getPendingIntent(IBinder binder);
void addPendingIntent(IBinder binder, String packageName);
void removePendingIntent(IBinder binder);
String getPackageForIntentSender(IBinder binder);
void processRestarted(in String packageName, in String processName, int userId);
void broadcastFinish(in PendingResultData res);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/IAppManager.aidl
================================================
// IAppManager.aidl
package com.lody.virtual.server;
import com.lody.virtual.server.interfaces.IPackageObserver;
import com.lody.virtual.server.interfaces.IAppRequestListener;
import com.lody.virtual.remote.InstalledAppInfo;
import com.lody.virtual.remote.InstallResult;
interface IAppManager {
int[] getPackageInstalledUsers(String packageName);
void scanApps();
void addVisibleOutsidePackage(String pkg);
void removeVisibleOutsidePackage(String pkg);
boolean isOutsidePackageVisible(String pkg);
InstalledAppInfo getInstalledAppInfo(String pkg, int flags);
InstallResult installPackage(String path, int flags);
boolean isPackageLaunched(int userId, String packageName);
void setPackageHidden(int userId, String packageName, boolean hidden);
boolean installPackageAsUser(int userId, String packageName);
boolean uninstallPackageAsUser(String packageName, int userId);
boolean uninstallPackage(String packageName);
List getInstalledApps(int flags);
List getInstalledAppsAsUser(int userId, int flags);
int getInstalledAppCount();
boolean isAppInstalled(String packageName);
boolean isAppInstalledAsUser(int userId, String packageName);
void registerObserver(IPackageObserver observer);
void unregisterObserver(IPackageObserver observer);
void setAppRequestListener(IAppRequestListener listener);
void clearAppRequestListener();
IAppRequestListener getAppRequestListener();
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/IBinderDelegateService.aidl
================================================
// IBinderDelegateService.aidl
package com.lody.virtual.server;
import android.content.ComponentName;
interface IBinderDelegateService {
ComponentName getComponent();
IBinder getService();
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/IDeviceInfoManager.aidl
================================================
// IDeviceInfoManager.aidl
package com.lody.virtual.server;
import com.lody.virtual.remote.VDeviceInfo;
interface IDeviceInfoManager {
VDeviceInfo getDeviceInfo(int userId);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/IJobScheduler.aidl
================================================
package com.lody.virtual.server;
import android.app.job.JobInfo;
/**
* IPC interface that supports the app-facing {@link #JobScheduler} api.
*/
interface IJobScheduler {
int schedule(in JobInfo job);
void cancel(int jobId);
void cancelAll();
List getAllPendingJobs();
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/INotificationManager.aidl
================================================
// INotificationManager.aidl
package com.lody.virtual.server;
// Declare any non-default types here with import statements
import android.app.Notification;
interface INotificationManager {
int dealNotificationId(int id, String packageName, String tag, int userId);
String dealNotificationTag(int id, String packageName, String tag, int userId);
boolean areNotificationsEnabledForPackage(String packageName, int userId);
void setNotificationsEnabledForPackage(String packageName, boolean enable, int userId);
void addNotification(int id, String tag, String packageName, int userId);
void cancelAllNotification(String packageName, int userId);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/IPackageInstaller.aidl
================================================
package com.lody.virtual.server;
import android.content.pm.IPackageDeleteObserver2;
import android.content.pm.IPackageInstallerCallback;
import android.content.pm.IPackageInstallerSession;
import android.content.IntentSender;
import android.graphics.Bitmap;
import com.lody.virtual.remote.VParceledListSlice;
import com.lody.virtual.server.pm.installer.SessionParams;
import com.lody.virtual.server.pm.installer.SessionInfo;
interface IPackageInstaller {
int createSession(in SessionParams params, String installerPackageName, int userId);
void updateSessionAppIcon(int sessionId, in Bitmap appIcon);
void updateSessionAppLabel(int sessionId, String appLabel);
void abandonSession(int sessionId);
IPackageInstallerSession openSession(int sessionId);
SessionInfo getSessionInfo(int sessionId);
VParceledListSlice getAllSessions(int userId);
VParceledListSlice getMySessions(String installerPackageName, int userId);
void registerCallback(IPackageInstallerCallback callback, int userId);
void unregisterCallback(IPackageInstallerCallback callback);
void uninstall(String packageName, String callerPackageName, int flags,
in IntentSender statusReceiver, int userId);
void setPermissionsResult(int sessionId, boolean accepted);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/IPackageInstallerSession.aidl
================================================
package com.lody.virtual.server;
import android.content.IntentSender;
import android.os.ParcelFileDescriptor;
interface IPackageInstallerSession {
void setClientProgress(float progress);
void addClientProgress(float progress);
String[] getNames();
ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes);
ParcelFileDescriptor openRead(String name);
void close();
void commit(in IntentSender statusReceiver);
void abandon();
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/IPackageManager.aidl
================================================
// IPackageManager.aidl
package com.lody.virtual.server;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.ActivityInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ApplicationInfo;
import android.content.IntentFilter;
import android.content.pm.PermissionInfo;
import android.content.pm.PermissionGroupInfo;
import com.lody.virtual.remote.ReceiverInfo;
import com.lody.virtual.remote.VParceledListSlice;
import com.lody.virtual.server.IPackageInstaller;
interface IPackageManager {
int getPackageUid(String packageName, int userId);
String[] getPackagesForUid(int vuid);
List getSharedLibraries(String pkgName);
int checkPermission(String permName, String pkgName, int userId);
PackageInfo getPackageInfo(String packageName, int flags, int userId);
ActivityInfo getActivityInfo(in ComponentName componentName, int flags, int userId);
boolean activitySupportsIntent(in ComponentName component, in Intent intent,
in String resolvedType);
ActivityInfo getReceiverInfo(in ComponentName componentName, int flags, int userId);
ServiceInfo getServiceInfo(in ComponentName componentName, int flags, int userId);
ProviderInfo getProviderInfo(in ComponentName componentName, int flags, int userId);
ResolveInfo resolveIntent(in Intent intent, in String resolvedType, int flags, int userId);
List queryIntentActivities(in Intent intent,in String resolvedType, int flags, int userId);
List queryIntentReceivers(in Intent intent, String resolvedType, int flags, int userId);
ResolveInfo resolveService(in Intent intent, String resolvedType, int flags, int userId);
List queryIntentServices(in Intent intent, String resolvedType, int flags, int userId);
List queryIntentContentProviders(in Intent intent, String resolvedType, int flags, int userId);
VParceledListSlice getInstalledPackages(int flags, int userId);
VParceledListSlice getInstalledApplications(int flags, int userId);
PermissionInfo getPermissionInfo(in String name, int flags);
List queryPermissionsByGroup(in String group, int flags);
PermissionGroupInfo getPermissionGroupInfo(in String name, int flags);
List getAllPermissionGroups(int flags);
ProviderInfo resolveContentProvider(in String name, int flags, int userId);
ApplicationInfo getApplicationInfo(in String packageName, int flags, int userId);
VParceledListSlice queryContentProviders(in String processName, int vuid, int flags);
List querySharedPackages(in String packageName);
String getNameForUid(int uid);
IPackageInstaller getPackageInstaller();
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/IUserManager.aidl
================================================
package com.lody.virtual.server;
import android.os.ParcelFileDescriptor;
import com.lody.virtual.os.VUserInfo;
import android.graphics.Bitmap;
/**
*
*/
interface IUserManager {
VUserInfo createUser(in String name, int flags);
boolean removeUser(int userHandle);
void setUserName(int userHandle, String name);
void setUserIcon(int userHandle, in Bitmap icon);
Bitmap getUserIcon(int userHandle);
List getUsers(boolean excludeDying);
VUserInfo getUserInfo(int userHandle);
void setGuestEnabled(boolean enable);
boolean isGuestEnabled();
void wipeUser(int userHandle);
int getUserSerialNumber(int userHandle);
int getUserHandle(int userSerialNumber);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/IVirtualStorageService.aidl
================================================
// IVirtualStorageService.aidl
package com.lody.virtual.server;
interface IVirtualStorageService {
void setVirtualStorage(in String packageName, in int userId, in String vsPath);
String getVirtualStorage(in String packageName, in int userId);
void setVirtualStorageState(in String packageName, in int userId, in boolean enable);
boolean isVirtualStorageEnable(in String packageName, in int userId);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/interfaces/IAppRequestListener.aidl
================================================
// IAppRequestListener.aidl
package com.lody.virtual.server.interfaces;
interface IAppRequestListener {
void onRequestInstall(in String path);
void onRequestUninstall(in String pkg);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/interfaces/IIntentFilterObserver.aidl
================================================
// IIntentFilterObserver.aidl
package com.lody.virtual.server.interfaces;
// Declare any non-default types here with import statements
interface IIntentFilterObserver {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
Intent filter(in Intent intent);
void setCallBack(IBinder callBack);
IBinder getCallBack();
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/interfaces/IPackageObserver.aidl
================================================
// IPackageObserver.aidl
package com.lody.virtual.server.interfaces;
interface IPackageObserver {
void onPackageInstalled(in String packageName);
void onPackageUninstalled(in String packageName);
void onPackageInstalledAsUser(in int userId, in String packageName);
void onPackageUninstalledAsUser(in int userId, in String packageName);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/interfaces/IProcessObserver.aidl
================================================
// IProcessObserver.aidl
package com.lody.virtual.server.interfaces;
interface IProcessObserver {
void onProcessCreated(in String pkg, in String processName);
void onProcessDied(in String pkg, in String processName);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/interfaces/IServiceFetcher.aidl
================================================
// IServiceFetcher.aidl
package com.lody.virtual.server.interfaces;
interface IServiceFetcher {
IBinder getService(String name);
void addService(String name,in IBinder service);
void removeService(String name);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/interfaces/IUiCallback.aidl
================================================
// IUiCallback.aidl
package com.lody.virtual.server.interfaces;
interface IUiCallback {
void onAppOpened(in String packageName, in int userId);
}
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/pm/installer/SessionInfo.aidl
================================================
// SessionInfo.aidl
package com.lody.virtual.server.pm.installer;
parcelable SessionInfo;
================================================
FILE: lib/src/main/aidl/com/lody/virtual/server/pm/installer/SessionParams.aidl
================================================
// SessionParams.aidl
package com.lody.virtual.server.pm.installer;
parcelable SessionParams;
================================================
FILE: lib/src/main/java/android/content/SyncStatusInfo.java
================================================
package android.content;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import java.util.ArrayList;
public class SyncStatusInfo implements Parcelable {
static final int VERSION = 2;
public final int authorityId;
public long totalElapsedTime;
public int numSyncs;
public int numSourcePoll;
public int numSourceServer;
public int numSourceLocal;
public int numSourceUser;
public int numSourcePeriodic;
public long lastSuccessTime;
public int lastSuccessSource;
public long lastFailureTime;
public int lastFailureSource;
public String lastFailureMesg;
public long initialFailureTime;
public boolean pending;
public boolean initialize;
// Warning: It is up to the external caller to ensure there are
// no race conditions when accessing this list
private ArrayList periodicSyncTimes;
private static final String TAG = "Sync";
public SyncStatusInfo(int authorityId) {
this.authorityId = authorityId;
}
public int getLastFailureMesgAsInt(int def) {
return 0;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeInt(VERSION);
parcel.writeInt(authorityId);
parcel.writeLong(totalElapsedTime);
parcel.writeInt(numSyncs);
parcel.writeInt(numSourcePoll);
parcel.writeInt(numSourceServer);
parcel.writeInt(numSourceLocal);
parcel.writeInt(numSourceUser);
parcel.writeLong(lastSuccessTime);
parcel.writeInt(lastSuccessSource);
parcel.writeLong(lastFailureTime);
parcel.writeInt(lastFailureSource);
parcel.writeString(lastFailureMesg);
parcel.writeLong(initialFailureTime);
parcel.writeInt(pending ? 1 : 0);
parcel.writeInt(initialize ? 1 : 0);
if (periodicSyncTimes != null) {
parcel.writeInt(periodicSyncTimes.size());
for (long periodicSyncTime : periodicSyncTimes) {
parcel.writeLong(periodicSyncTime);
}
} else {
parcel.writeInt(-1);
}
}
public SyncStatusInfo(Parcel parcel) {
int version = parcel.readInt();
if (version != VERSION && version != 1) {
Log.w("SyncStatusInfo", "Unknown version: " + version);
}
authorityId = parcel.readInt();
totalElapsedTime = parcel.readLong();
numSyncs = parcel.readInt();
numSourcePoll = parcel.readInt();
numSourceServer = parcel.readInt();
numSourceLocal = parcel.readInt();
numSourceUser = parcel.readInt();
lastSuccessTime = parcel.readLong();
lastSuccessSource = parcel.readInt();
lastFailureTime = parcel.readLong();
lastFailureSource = parcel.readInt();
lastFailureMesg = parcel.readString();
initialFailureTime = parcel.readLong();
pending = parcel.readInt() != 0;
initialize = parcel.readInt() != 0;
if (version == 1) {
periodicSyncTimes = null;
} else {
int N = parcel.readInt();
if (N < 0) {
periodicSyncTimes = null;
} else {
periodicSyncTimes = new ArrayList();
for (int i=0; i(other.periodicSyncTimes);
}
}
public void setPeriodicSyncTime(int index, long when) {
// The list is initialized lazily when scheduling occurs so we need to make sure
// we initialize elements < index to zero (zero is ignore for scheduling purposes)
ensurePeriodicSyncTimeSize(index);
periodicSyncTimes.set(index, when);
}
public long getPeriodicSyncTime(int index) {
if (periodicSyncTimes != null && index < periodicSyncTimes.size()) {
return periodicSyncTimes.get(index);
} else {
return 0;
}
}
public void removePeriodicSyncTime(int index) {
if (periodicSyncTimes != null && index < periodicSyncTimes.size()) {
periodicSyncTimes.remove(index);
}
}
public static final Creator CREATOR = new Creator() {
public SyncStatusInfo createFromParcel(Parcel in) {
return new SyncStatusInfo(in);
}
public SyncStatusInfo[] newArray(int size) {
return new SyncStatusInfo[size];
}
};
private void ensurePeriodicSyncTimeSize(int index) {
if (periodicSyncTimes == null) {
periodicSyncTimes = new ArrayList<>(0);
}
final int requiredSize = index + 1;
if (periodicSyncTimes.size() < requiredSize) {
for (int i = periodicSyncTimes.size(); i < requiredSize; i++) {
periodicSyncTimes.add((long) 0);
}
}
}
}
================================================
FILE: lib/src/main/java/android/content/pm/PackageParser.java
================================================
package android.content.pm;
import android.content.ComponentName;
import android.content.IntentFilter;
import android.os.Bundle;
import java.util.ArrayList;
/**
* @author Lody
*/
public class PackageParser {
public static final int PARSE_IS_SYSTEM = 1;
public static class IntentInfo extends IntentFilter {
public boolean hasDefault;
public int labelRes;
public CharSequence nonLocalizedLabel;
public int icon;
public int logo;
public int banner;
}
public static class Component {
public Package owner;
public ArrayList intents;
public String className;
public Bundle metaData;
public ComponentName getComponentName() {
return null;
}
}
public final static class Activity extends Component {
public ActivityInfo info;
}
// 中间包组件结构
public class Package {
public final ArrayList activities = new ArrayList(0);
// Receiver 结构与 Activity 类似
public final ArrayList receivers = new ArrayList(0);
public final ArrayList providers = new ArrayList(0);
public final ArrayList services = new ArrayList(0);
public final ArrayList instrumentation = new ArrayList(0);
public final ArrayList permissions = new ArrayList(0);
public final ArrayList permissionGroups = new ArrayList(0);
public final ArrayList requestedPermissions = new ArrayList();
public Signature[] mSignatures;
public Bundle mAppMetaData;
public Object mExtras;
public String packageName;
public int mPreferredOrder;
public String mSharedUserId;
public ArrayList usesLibraries;
public int mVersionCode;
public ApplicationInfo applicationInfo;
public String mVersionName;
// Applications hardware preferences
public ArrayList configPreferences = null;
// Applications requested features
public ArrayList reqFeatures = null;
public int mSharedUserLabel;
}
public final class Service extends Component {
public ServiceInfo info;
}
public final class Provider extends Component {
public ProviderInfo info;
}
public final class Instrumentation extends Component {
public InstrumentationInfo info;
}
public final class Permission extends Component {
public PermissionInfo info;
}
public final class PermissionGroup extends Component {
public PermissionGroupInfo info;
}
public class ActivityIntentInfo extends IntentInfo {
public Activity activity;
}
public class ServiceIntentInfo extends IntentInfo {
public Service service;
}
public class ProviderIntentInfo extends IntentInfo {
public Provider provider;
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/Build.java
================================================
package com.lody.virtual;
/**
*
* Version info of VirtualApp project.
*
* @author Lody
*
*/
public class Build {
public static final String VERSION_NAME = "Build-823-01";
public static final int VERSION_CODE = 8230001;
}
================================================
FILE: lib/src/main/java/com/lody/virtual/GmsSupport.java
================================================
package com.lody.virtual;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import com.lody.virtual.client.core.InstallStrategy;
import com.lody.virtual.client.core.VirtualCore;
import java.util.Arrays;
import java.util.List;
/**
* @author Lody
*/
public class GmsSupport {
private static final List GOOGLE_APP = Arrays.asList(
"com.android.vending",
"com.google.android.play.games",
"com.google.android.wearable.app",
"com.google.android.wearable.app.cn"
);
private static final List GOOGLE_SERVICE = Arrays.asList(
"com.google.android.gsf",
"com.google.android.gms",
"com.google.android.gsf.login",
"com.google.android.backuptransport",
"com.google.android.backup",
"com.google.android.configupdater",
"com.google.android.syncadapters.contacts",
"com.google.android.feedback",
"com.google.android.onetimeinitializer",
"com.google.android.partnersetup",
"com.google.android.setupwizard",
"com.google.android.syncadapters.calendar"
);
public static boolean isGmsFamilyPackage(String packageName) {
return packageName.equals("com.android.vending")
|| packageName.equals("com.google.android.gms");
}
public static boolean isGoogleFrameworkInstalled() {
return VirtualCore.get().isAppInstalled("com.google.android.gms");
}
public static boolean isOutsideGoogleFrameworkExist() {
return VirtualCore.get().isOutsideInstalled("com.google.android.gms");
}
private static void installPackages(List list, int userId) {
VirtualCore core = VirtualCore.get();
for (String packageName : list) {
if (core.isAppInstalledAsUser(userId, packageName)) {
continue;
}
ApplicationInfo info = null;
try {
info = VirtualCore.get().getUnHookPackageManager().getApplicationInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
// Ignore
}
if (info == null || info.sourceDir == null) {
continue;
}
if (userId == 0) {
core.installPackage(info.sourceDir, InstallStrategy.DEPEND_SYSTEM_IF_EXIST);
} else {
core.installPackageAsUser(userId, packageName);
}
}
}
public static void installGApps(int userId) {
installPackages(GOOGLE_SERVICE, userId);
installPackages(GOOGLE_APP, userId);
}
public static void installGoogleService(int userId) {
installPackages(GOOGLE_SERVICE, userId);
}
public static void installGoogleApp(int userId) {
installPackages(GOOGLE_APP, userId);
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/NativeEngine.java
================================================
package com.lody.virtual.client;
import android.annotation.SuppressLint;
import android.os.Binder;
import android.os.Build;
import android.os.Process;
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.client.env.VirtualRuntime;
import com.lody.virtual.client.ipc.VActivityManager;
import com.lody.virtual.client.natives.NativeMethods;
import com.lody.virtual.helper.compat.BuildCompat;
import com.lody.virtual.helper.utils.VLog;
import com.lody.virtual.os.VUserHandle;
import com.lody.virtual.remote.InstalledAppInfo;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* VirtualApp Native Project
*/
public class NativeEngine {
private static final String TAG = NativeEngine.class.getSimpleName();
private static Map sDexOverrideMap;
private static boolean sFlag = false;
static {
try {
System.loadLibrary("va-native");
} catch (Throwable e) {
VLog.e(TAG, VLog.getStackTraceString(e));
}
}
static {
NativeMethods.init();
}
public static void startDexOverride() {
List installedAppInfos = VirtualCore.get().getInstalledApps(0);
sDexOverrideMap = new HashMap<>(installedAppInfos.size());
for (InstalledAppInfo info : installedAppInfos) {
try {
sDexOverrideMap.put(new File(info.apkPath).getCanonicalPath(), info);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static String getRedirectedPath(String origPath) {
try {
return nativeGetRedirectedPath(origPath);
} catch (Throwable e) {
VLog.e(TAG, VLog.getStackTraceString(e));
}
return origPath;
}
public static String restoreRedirectedPath(String origPath) {
try {
return nativeRestoreRedirectedPath(origPath);
} catch (Throwable e) {
VLog.e(TAG, VLog.getStackTraceString(e));
}
return origPath;
}
public static void redirectDirectory(String origPath, String newPath) {
if (!origPath.endsWith("/")) {
origPath = origPath + "/";
}
if (!newPath.endsWith("/")) {
newPath = newPath + "/";
}
try {
nativeRedirect(origPath, newPath);
} catch (Throwable e) {
VLog.e(TAG, VLog.getStackTraceString(e));
}
}
public static void redirectFile(String origPath, String newPath) {
if (origPath.endsWith("/")) {
origPath = origPath.substring(0, origPath.length() - 1);
}
if (newPath.endsWith("/")) {
newPath = newPath.substring(0, newPath.length() - 1);
}
try {
nativeRedirect(origPath, newPath);
} catch (Throwable e) {
VLog.e(TAG, VLog.getStackTraceString(e));
}
}
public static void readOnly(String path) {
try {
nativeReadOnly(path);
} catch (Throwable e) {
VLog.e(TAG, VLog.getStackTraceString(e));
}
}
public static void hook() {
try {
String soPath = String.format("/data/data/%s/lib/libva-native.so", VirtualCore.get().getHostPkg());
if (!new File(soPath).exists()) {
throw new RuntimeException("Unable to find the so.");
}
nativeStartUniformer(soPath, Build.VERSION.SDK_INT, BuildCompat.getPreviewSDKInt());
} catch (Throwable e) {
VLog.e(TAG, VLog.getStackTraceString(e));
}
}
// hook native 函数,目前 openDexFile,Camera,AudioRecoder
static void hookNative() {
if (sFlag) {
return;
}
Method[] methods = {NativeMethods.gOpenDexFileNative, NativeMethods.gCameraNativeSetup, NativeMethods.gAudioRecordNativeCheckPermission};
try {
nativeHookNative(methods, VirtualCore.get().getHostPkg(), VirtualRuntime.isArt(), Build.VERSION.SDK_INT, NativeMethods.gCameraMethodType);
} catch (Throwable e) {
VLog.e(TAG, VLog.getStackTraceString(e));
}
sFlag = true;
}
public static void onKillProcess(int pid, int signal) {
VLog.e(TAG, "killProcess: pid = %d, signal = %d.", pid, signal);
if (pid == android.os.Process.myPid()) {
VLog.e(TAG, VLog.getStackTraceString(new Throwable()));
}
}
public static int onGetCallingUid(int originUid) {
int callingPid = Binder.getCallingPid();
if (callingPid == Process.myPid()) {
return VClientImpl.get().getBaseVUid();
}
if (callingPid == VirtualCore.get().getSystemPid()) {
return Process.SYSTEM_UID;
}
int vuid = VActivityManager.get().getUidByPid(callingPid);
if (vuid != -1) {
return VUserHandle.getAppId(vuid);
}
VLog.d(TAG, "Unknown uid: " + callingPid);
return VClientImpl.get().getBaseVUid();
}
// hook 的 openDexFile 函数,native 层重定向到 java 层
public static void onOpenDexFileNative(String[] params) {
String dexOrJarPath = params[0];
String outputPath = params[1];
VLog.d(TAG, "DexOrJarPath = %s, OutputPath = %s.", dexOrJarPath, outputPath);
try {
String canonical = new File(dexOrJarPath).getCanonicalPath();
InstalledAppInfo info = sDexOverrideMap.get(canonical);
if (info != null && !info.dependSystem) {
outputPath = info.getOdexFile().getPath();
params[1] = outputPath;
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static native void nativeHookNative(Object method, String hostPackageName, boolean isArt, int apiLevel, int cameraMethodType);
private static native void nativeMark();
private static native String nativeRestoreRedirectedPath(String redirectedPath);
private static native String nativeGetRedirectedPath(String orgPath);
private static native void nativeRedirect(String origPath, String newPath);
private static native void nativeReadOnly(String path);
private static native void nativeStartUniformer(String selfSoPath, int apiLevel, int previewApiLevel);
public static int onGetUid(int uid) {
return VClientImpl.get().getBaseVUid();
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/VClientImpl.java
================================================
package com.lody.virtual.client;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.Instrumentation;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.os.Binder;
import android.os.Build;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.os.StrictMode;
import android.util.Log;
import com.lody.virtual.client.core.CrashHandler;
import com.lody.virtual.client.core.InvocationStubManager;
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.client.env.SpecialComponentList;
import com.lody.virtual.client.env.VirtualRuntime;
import com.lody.virtual.client.fixer.ContextFixer;
import com.lody.virtual.client.hook.delegate.AppInstrumentation;
import com.lody.virtual.client.hook.providers.ProviderHook;
import com.lody.virtual.client.hook.proxies.am.HCallbackStub;
import com.lody.virtual.client.hook.secondary.ProxyServiceFactory;
import com.lody.virtual.client.ipc.VActivityManager;
import com.lody.virtual.client.ipc.VDeviceManager;
import com.lody.virtual.client.ipc.VPackageManager;
import com.lody.virtual.client.ipc.VirtualStorageManager;
import com.lody.virtual.client.stub.VASettings;
import com.lody.virtual.helper.compat.BuildCompat;
import com.lody.virtual.helper.compat.StorageManagerCompat;
import com.lody.virtual.helper.utils.VLog;
import com.lody.virtual.os.VEnvironment;
import com.lody.virtual.os.VUserHandle;
import com.lody.virtual.remote.InstalledAppInfo;
import com.lody.virtual.remote.PendingResultData;
import com.lody.virtual.remote.VDeviceInfo;
import com.taobao.android.dex.interpret.ARTUtils;
import com.taobao.android.runtime.DalvikUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import mirror.android.app.ActivityThread;
import mirror.android.app.ActivityThreadNMR1;
import mirror.android.app.ContextImpl;
import mirror.android.app.IActivityManager;
import mirror.android.app.LoadedApk;
import mirror.android.content.ContentProviderHolderOreo;
import mirror.android.providers.Settings;
import mirror.android.renderscript.RenderScriptCacheDir;
import mirror.android.view.HardwareRenderer;
import mirror.android.view.RenderScript;
import mirror.android.view.ThreadedRenderer;
import mirror.com.android.internal.content.ReferrerIntent;
import mirror.dalvik.system.VMRuntime;
import mirror.java.lang.ThreadGroupN;
import static com.lody.virtual.os.VUserHandle.getUserId;
/**
* @author Lody
*/
public final class VClientImpl extends IVClient.Stub {
private static final int NEW_INTENT = 11;
private static final int RECEIVER = 12;
private static final String TAG = VClientImpl.class.getSimpleName();
@SuppressLint("StaticFieldLeak")
private static final VClientImpl gClient = new VClientImpl();
private final H mH = new H();
private ConditionVariable mTempLock;
private Instrumentation mInstrumentation = AppInstrumentation.getDefault();
private IBinder token;
private int vuid;
private VDeviceInfo deviceInfo;
private AppBindData mBoundApplication;
private Application mInitialApplication;
private CrashHandler crashHandler;
public static VClientImpl get() {
return gClient;
}
public boolean isBound() {
return mBoundApplication != null;
}
public VDeviceInfo getDeviceInfo() {
return deviceInfo;
}
public Application getCurrentApplication() {
return mInitialApplication;
}
public String getCurrentPackage() {
return mBoundApplication != null ? mBoundApplication.appInfo.packageName : null;
}
public ApplicationInfo getCurrentApplicationInfo() {
return mInitialApplication != null ? mInitialApplication.getApplicationInfo() : null;
}
public CrashHandler getCrashHandler() {
return crashHandler;
}
public void setCrashHandler(CrashHandler crashHandler) {
this.crashHandler = crashHandler;
}
public int getVUid() {
return vuid;
}
public int getBaseVUid() {
return VUserHandle.getAppId(vuid);
}
public ClassLoader getClassLoader(ApplicationInfo appInfo) {
Context context = createPackageContext(appInfo.packageName);
return context.getClassLoader();
}
private void sendMessage(int what, Object obj) {
Message msg = Message.obtain();
msg.what = what;
msg.obj = obj;
mH.sendMessage(msg);
}
@Override
public IBinder getAppThread() {
return ActivityThread.getApplicationThread.call(VirtualCore.mainThread());
}
@Override
public IBinder getToken() {
return token;
}
public void initProcess(IBinder token, int vuid) {
this.token = token;
this.vuid = vuid;
this.deviceInfo = VDeviceManager.get().getDeviceInfo(getUserId(vuid));
}
private void handleNewIntent(NewIntentData data) {
Intent intent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
intent = ReferrerIntent.ctor.newInstance(data.intent, data.creator);
} else {
intent = data.intent;
}
if (ActivityThread.performNewIntents != null) {
ActivityThread.performNewIntents.call(
VirtualCore.mainThread(),
data.token,
Collections.singletonList(intent)
);
} else {
ActivityThreadNMR1.performNewIntents.call(
VirtualCore.mainThread(),
data.token,
Collections.singletonList(intent),
true);
}
}
public void bindApplication(final String packageName, final String processName) {
if (Looper.getMainLooper() == Looper.myLooper()) {
bindApplicationNoCheck(packageName, processName, new ConditionVariable());
} else {
final ConditionVariable lock = new ConditionVariable();
VirtualRuntime.getUIHandler().post(new Runnable() {
@Override
public void run() {
bindApplicationNoCheck(packageName, processName, lock);
lock.open();
}
});
lock.block();
}
}
// 该逻辑参考 framework 的 ActivityThread.handleBindApplication
private void bindApplicationNoCheck(String packageName, String processName, ConditionVariable lock) {
mTempLock = lock;
try {
// 设置未捕获异常的 Callback
setupUncaughtHandler();
} catch (Throwable e) {
e.printStackTrace();
}
try {
// 修复 Provider 信息
fixInstalledProviders();
} catch (Throwable e) {
e.printStackTrace();
}
mirror.android.os.Build.SERIAL.set(deviceInfo.serial);
mirror.android.os.Build.DEVICE.set(Build.DEVICE.replace(" ", "_"));
ActivityThread.mInitialApplication.set(
VirtualCore.mainThread(),
null
);
// 从 VPMS 获取 apk 信息
AppBindData data = new AppBindData();
InstalledAppInfo info = VirtualCore.get().getInstalledAppInfo(packageName, 0);
if (info == null) {
new Exception("App not exist!").printStackTrace();
Process.killProcess(0);
System.exit(0);
}
// dex 优化的开关,dalvik 和 art 处理不同
if (!info.dependSystem && info.skipDexOpt) {
VLog.d(TAG, "Dex opt skipped.");
if (VirtualRuntime.isArt()) {
ARTUtils.init(VirtualCore.get().getContext());
ARTUtils.setIsDex2oatEnabled(false);
} else {
DalvikUtils.init();
DalvikUtils.setDexOptMode(DalvikUtils.OPTIMIZE_MODE_NONE);
}
}
data.appInfo = VPackageManager.get().getApplicationInfo(packageName, 0, getUserId(vuid));
data.processName = processName;
data.providers = VPackageManager.get().queryContentProviders(processName, getVUid(), PackageManager.GET_META_DATA);
Log.i(TAG, "Binding application " + data.appInfo.packageName + " (" + data.processName + ")");
mBoundApplication = data;
// 主要设置进程的名字
VirtualRuntime.setupRuntime(data.processName, data.appInfo);
int targetSdkVersion = data.appInfo.targetSdkVersion;
if (targetSdkVersion < Build.VERSION_CODES.GINGERBREAD) {
StrictMode.ThreadPolicy newPolicy = new StrictMode.ThreadPolicy.Builder(StrictMode.getThreadPolicy()).permitNetwork().build();
StrictMode.setThreadPolicy(newPolicy);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (mirror.android.os.StrictMode.sVmPolicyMask != null) {
mirror.android.os.StrictMode.sVmPolicyMask.set(0);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && targetSdkVersion < Build.VERSION_CODES.LOLLIPOP) {
mirror.android.os.Message.updateCheckRecycle.call(targetSdkVersion);
}
if (VASettings.ENABLE_IO_REDIRECT) {
// IO 重定向
startIOUniformer();
}
// hook native 函数
NativeEngine.hookNative();
Object mainThread = VirtualCore.mainThread();
// 准备 dex 列表
NativeEngine.startDexOverride();
// 获得子 pkg 的 Context 前提是必须在系统中安装的(疑问?)
Context context = createPackageContext(data.appInfo.packageName);
// 设置虚拟机系统环境 临时文件夹 codeCacheDir
System.setProperty("java.io.tmpdir", context.getCacheDir().getAbsolutePath());
// oat 的 cache 目录
File codeCacheDir;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
codeCacheDir = context.getCodeCacheDir();
} else {
codeCacheDir = context.getCacheDir();
}
// 硬件加速的 cache 目录
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
if (HardwareRenderer.setupDiskCache != null) {
HardwareRenderer.setupDiskCache.call(codeCacheDir);
}
} else {
if (ThreadedRenderer.setupDiskCache != null) {
ThreadedRenderer.setupDiskCache.call(codeCacheDir);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (RenderScriptCacheDir.setupDiskCache != null) {
RenderScriptCacheDir.setupDiskCache.call(codeCacheDir);
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
if (RenderScript.setupDiskCache != null) {
RenderScript.setupDiskCache.call(codeCacheDir);
}
}
// 修复子 App 中 ActivityThread.AppBind信息erData 的参数,因为之前用的是在 Host 程序中注册的 Stub 的信息
Object boundApp = fixBoundApp(mBoundApplication);
mBoundApplication.info = ContextImpl.mPackageInfo.get(context);
mirror.android.app.ActivityThread.AppBindData.info.set(boundApp, data.info);
// 同样修复 targetSdkVersion 原来也是可 Host 程序一样的
VMRuntime.setTargetSdkVersion.call(VMRuntime.getRuntime.call(), data.appInfo.targetSdkVersion);
boolean conflict = SpecialComponentList.isConflictingInstrumentation(packageName);
if (!conflict) {
InvocationStubManager.getInstance().checkEnv(AppInstrumentation.class);
}
// 开始构建子程序包的 Application 对象,并且替换原来通过 Host Stub 生成的 mInitialApplication
mInitialApplication = LoadedApk.makeApplication.call(data.info, false, null);
mirror.android.app.ActivityThread.mInitialApplication.set(mainThread, mInitialApplication);
ContextFixer.fixContext(mInitialApplication);
if (data.providers != null) {
// 注册 Providers
installContentProviders(mInitialApplication, data.providers);
}
// 初始化锁开,异步调用的初始化函数可以返回了
if (lock != null) {
lock.open();
mTempLock = null;
}
try {
// 调用 Application.onCreate
mInstrumentation.callApplicationOnCreate(mInitialApplication);
InvocationStubManager.getInstance().checkEnv(HCallbackStub.class);
if (conflict) {
InvocationStubManager.getInstance().checkEnv(AppInstrumentation.class);
}
Application createdApp = ActivityThread.mInitialApplication.get(mainThread);
if (createdApp != null) {
mInitialApplication = createdApp;
}
} catch (Exception e) {
if (!mInstrumentation.onException(mInitialApplication, e)) {
throw new RuntimeException(
"Unable to create application " + mInitialApplication.getClass().getName()
+ ": " + e.toString(), e);
}
}
VActivityManager.get().appDoneExecuting();
}
private void setupUncaughtHandler() {
ThreadGroup root = Thread.currentThread().getThreadGroup();
while (root.getParent() != null) {
root = root.getParent();
}
ThreadGroup newRoot = new RootThreadGroup(root);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
final List groups = mirror.java.lang.ThreadGroup.groups.get(root);
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (groups) {
List newGroups = new ArrayList<>(groups);
newGroups.remove(newRoot);
mirror.java.lang.ThreadGroup.groups.set(newRoot, newGroups);
groups.clear();
groups.add(newRoot);
mirror.java.lang.ThreadGroup.groups.set(root, groups);
for (ThreadGroup group : newGroups) {
mirror.java.lang.ThreadGroup.parent.set(group, newRoot);
}
}
} else {
final ThreadGroup[] groups = ThreadGroupN.groups.get(root);
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (groups) {
ThreadGroup[] newGroups = groups.clone();
ThreadGroupN.groups.set(newRoot, newGroups);
ThreadGroupN.groups.set(root, new ThreadGroup[]{newRoot});
for (Object group : newGroups) {
ThreadGroupN.parent.set(group, newRoot);
}
ThreadGroupN.ngroups.set(root, 1);
}
}
}
// IO 重定向
@SuppressLint("SdCardPath")
private void startIOUniformer() {
ApplicationInfo info = mBoundApplication.appInfo;
int userId = VUserHandle.myUserId();
String wifiMacAddressFile = deviceInfo.getWifiFile(userId).getPath();
NativeEngine.redirectDirectory("/sys/class/net/wlan0/address", wifiMacAddressFile);
NativeEngine.redirectDirectory("/sys/class/net/eth0/address", wifiMacAddressFile);
NativeEngine.redirectDirectory("/sys/class/net/wifi/address", wifiMacAddressFile);
NativeEngine.redirectDirectory("/data/data/" + info.packageName, info.dataDir);
NativeEngine.redirectDirectory("/data/user/0/" + info.packageName, info.dataDir);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
NativeEngine.redirectDirectory("/data/user_de/0/" + info.packageName, info.dataDir);
}
String libPath = new File(VEnvironment.getDataAppPackageDirectory(info.packageName), "lib").getAbsolutePath();
String userLibPath = new File(VEnvironment.getUserSystemDirectory(userId), "lib").getAbsolutePath();
NativeEngine.redirectDirectory(userLibPath, libPath);
NativeEngine.redirectDirectory("/data/data/" + info.packageName + "/lib/", libPath);
NativeEngine.redirectDirectory("/data/user/0/" + info.packageName + "/lib/", libPath);
NativeEngine.readOnly(VEnvironment.getDataAppDirectory().getPath());
VirtualStorageManager vsManager = VirtualStorageManager.get();
String vsPath = vsManager.getVirtualStorage(info.packageName, userId);
boolean enable = vsManager.isVirtualStorageEnable(info.packageName, userId);
if (enable && vsPath != null) {
File vsDirectory = new File(vsPath);
if (vsDirectory.exists() || vsDirectory.mkdirs()) {
HashSet mountPoints = getMountPoints();
for (String mountPoint : mountPoints) {
NativeEngine.redirectDirectory(mountPoint, vsPath);
}
}
}
NativeEngine.hook();
}
@SuppressLint("SdCardPath")
private HashSet getMountPoints() {
HashSet mountPoints = new HashSet<>(3);
mountPoints.add("/mnt/sdcard/");
mountPoints.add("/sdcard/");
String[] points = StorageManagerCompat.getAllPoints(VirtualCore.get().getContext());
if (points != null) {
Collections.addAll(mountPoints, points);
}
return mountPoints;
}
private Context createPackageContext(String packageName) {
try {
Log.e("gy", "create pkg context for pkg: " + packageName);
Context hostContext = VirtualCore.get().getContext();
return hostContext.createPackageContext(packageName, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
} catch (PackageManager.NameNotFoundException e) {
VirtualRuntime.crash(new RemoteException());
}
throw new RuntimeException();
}
private Object fixBoundApp(AppBindData data) {
Object thread = VirtualCore.mainThread();
Object boundApp = mirror.android.app.ActivityThread.mBoundApplication.get(thread);
mirror.android.app.ActivityThread.AppBindData.appInfo.set(boundApp, data.appInfo);
mirror.android.app.ActivityThread.AppBindData.processName.set(boundApp, data.processName);
mirror.android.app.ActivityThread.AppBindData.instrumentationName.set(
boundApp,
new ComponentName(data.appInfo.packageName, Instrumentation.class.getName())
);
ActivityThread.AppBindData.providers.set(boundApp, data.providers);
return boundApp;
}
private void installContentProviders(Context app, List providers) {
long origId = Binder.clearCallingIdentity();
Object mainThread = VirtualCore.mainThread();
try {
for (ProviderInfo cpi : providers) {
if (cpi.enabled) {
ActivityThread.installProvider(mainThread, app, cpi, null);
}
}
} finally {
Binder.restoreCallingIdentity(origId);
}
}
@Override
public IBinder acquireProviderClient(ProviderInfo info) {
if (mTempLock != null) {
mTempLock.block();
}
// 这里检查 Application 是否启动,注意注册 Provider 的逻辑也在里面
if (!isBound()) {
VClientImpl.get().bindApplication(info.packageName, info.processName);
}
// 准备 ContentProviderClient
IInterface provider = null;
String[] authorities = info.authority.split(";");
String authority = authorities.length == 0 ? info.authority : authorities[0];
ContentResolver resolver = VirtualCore.get().getContext().getContentResolver();
ContentProviderClient client = null;
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
client = resolver.acquireUnstableContentProviderClient(authority);
} else {
client = resolver.acquireContentProviderClient(authority);
}
} catch (Throwable e) {
e.printStackTrace();
}
if (client != null) {
// 反射获取 provider
provider = mirror.android.content.ContentProviderClient.mContentProvider.get(client);
client.release();
}
return provider != null ? provider.asBinder() : null;
}
private void fixInstalledProviders() {
clearSettingProvider();
Map clientMap = ActivityThread.mProviderMap.get(VirtualCore.mainThread());
for (Object clientRecord : clientMap.values()) {
if (BuildCompat.isOreo()) {
IInterface provider = ActivityThread.ProviderClientRecordJB.mProvider.get(clientRecord);
Object holder = ActivityThread.ProviderClientRecordJB.mHolder.get(clientRecord);
if (holder == null) {
continue;
}
ProviderInfo info = ContentProviderHolderOreo.info.get(holder);
if (!info.authority.startsWith(VASettings.STUB_CP_AUTHORITY)) {
provider = ProviderHook.createProxy(true, info.authority, provider);
ActivityThread.ProviderClientRecordJB.mProvider.set(clientRecord, provider);
ContentProviderHolderOreo.provider.set(holder, provider);
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
IInterface provider = ActivityThread.ProviderClientRecordJB.mProvider.get(clientRecord);
Object holder = ActivityThread.ProviderClientRecordJB.mHolder.get(clientRecord);
if (holder == null) {
continue;
}
ProviderInfo info = IActivityManager.ContentProviderHolder.info.get(holder);
if (!info.authority.startsWith(VASettings.STUB_CP_AUTHORITY)) {
provider = ProviderHook.createProxy(true, info.authority, provider);
ActivityThread.ProviderClientRecordJB.mProvider.set(clientRecord, provider);
IActivityManager.ContentProviderHolder.provider.set(holder, provider);
}
} else {
String authority = ActivityThread.ProviderClientRecord.mName.get(clientRecord);
IInterface provider = ActivityThread.ProviderClientRecord.mProvider.get(clientRecord);
if (provider != null && !authority.startsWith(VASettings.STUB_CP_AUTHORITY)) {
provider = ProviderHook.createProxy(true, authority, provider);
ActivityThread.ProviderClientRecord.mProvider.set(clientRecord, provider);
}
}
}
}
private void clearSettingProvider() {
Object cache;
cache = Settings.System.sNameValueCache.get();
if (cache != null) {
clearContentProvider(cache);
}
cache = Settings.Secure.sNameValueCache.get();
if (cache != null) {
clearContentProvider(cache);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && Settings.Global.TYPE != null) {
cache = Settings.Global.sNameValueCache.get();
if (cache != null) {
clearContentProvider(cache);
}
}
}
private static void clearContentProvider(Object cache) {
if (BuildCompat.isOreo()) {
Object holder = Settings.NameValueCacheOreo.mProviderHolder.get(cache);
if (holder != null) {
Settings.ContentProviderHolder.mContentProvider.set(holder, null);
}
} else {
Settings.NameValueCache.mContentProvider.set(cache, null);
}
}
@Override
public void finishActivity(IBinder token) {
VActivityManager.get().finishActivity(token);
}
// 将被 ActivityStack 远程调用
@Override
public void scheduleNewIntent(String creator, IBinder token, Intent intent) {
NewIntentData data = new NewIntentData();
data.creator = creator;
data.token = token;
data.intent = intent;
sendMessage(NEW_INTENT, data);
}
@Override
public void scheduleReceiver(String processName, ComponentName component, Intent intent, PendingResultData resultData) {
ReceiverData receiverData = new ReceiverData();
receiverData.resultData = resultData;
receiverData.intent = intent;
receiverData.component = component;
receiverData.processName = processName;
sendMessage(RECEIVER, receiverData);
}
private void handleReceiver(ReceiverData data) {
BroadcastReceiver.PendingResult result = data.resultData.build();
try {
// 依然是检测 Application 是否初始化,没有则初始化
if (!isBound()) {
bindApplication(data.component.getPackageName(), data.processName);
}
// 获取 Receiver 的 Context,这个context是一个ReceiverRestrictedContext实例,它有两个主要函数被禁掉:registerReceiver()和 bindService()。这两个函数在BroadcastReceiver.onReceive()不允许调用。每次Receiver处理一个广播,传递进来的context都是一个新的实例。
Context context = mInitialApplication.getBaseContext();
Context receiverContext = ContextImpl.getReceiverRestrictedContext.call(context);
String className = data.component.getClassName();
Log.e("gy", "handler Receiver: " + className);
// 实例化目标 Receiver
BroadcastReceiver receiver = (BroadcastReceiver) context.getClassLoader().loadClass(className).newInstance();
mirror.android.content.BroadcastReceiver.setPendingResult.call(receiver, result);
data.intent.setExtrasClassLoader(context.getClassLoader());
// 手动调用 onCreate
receiver.onReceive(receiverContext, data.intent);
// 通知 Pending 结束
if (mirror.android.content.BroadcastReceiver.getPendingResult.call(receiver) != null) {
result.finish();
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(
"Unable to start receiver " + data.component
+ ": " + e.toString(), e);
}
// 这里需要远程通知 VAService 广播已送到
VActivityManager.get().broadcastFinish(data.resultData);
}
@Override
public IBinder createProxyService(ComponentName component, IBinder binder) {
return ProxyServiceFactory.getProxyService(getCurrentApplication(), component, binder);
}
@Override
public String getDebugInfo() {
return "process : " + VirtualRuntime.getProcessName() + "\n" +
"initialPkg : " + VirtualRuntime.getInitialPackageName() + "\n" +
"vuid : " + vuid;
}
private static class RootThreadGroup extends ThreadGroup {
RootThreadGroup(ThreadGroup parent) {
super(parent, "VA-Root");
}
@Override
public void uncaughtException(Thread t, Throwable e) {
CrashHandler handler = VClientImpl.gClient.crashHandler;
if (handler != null) {
handler.handleUncaughtException(t, e);
} else {
VLog.e("uncaught", e);
System.exit(0);
}
}
}
private final class NewIntentData {
String creator;
IBinder token;
Intent intent;
}
private final class AppBindData {
String processName;
ApplicationInfo appInfo;
List providers;
Object info;
}
private final class ReceiverData {
PendingResultData resultData;
Intent intent;
ComponentName component;
String processName;
}
private class H extends Handler {
private H() {
super(Looper.getMainLooper());
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case NEW_INTENT: {
handleNewIntent((NewIntentData) msg.obj);
}
break;
case RECEIVER: {
handleReceiver((ReceiverData) msg.obj);
}
}
}
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/core/CrashHandler.java
================================================
package com.lody.virtual.client.core;
/**
* @author Lody
*/
public interface CrashHandler {
void handleUncaughtException(Thread t, Throwable e);
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/core/InstallStrategy.java
================================================
package com.lody.virtual.client.core;
/**
* @author Lody
*
*
*/
public interface InstallStrategy {
int TERMINATE_IF_EXIST = 0x01 << 1;
int UPDATE_IF_EXIST = 0x01 << 2;
int COMPARE_VERSION = 0X01 << 3;
int IGNORE_NEW_VERSION = 0x01 << 4;
int DEPEND_SYSTEM_IF_EXIST = 0x01 << 5;
int SKIP_DEX_OPT = 0x01 << 6;
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/core/InvocationStubManager.java
================================================
package com.lody.virtual.client.core;
import android.os.Build;
import com.lody.virtual.client.hook.base.MethodInvocationProxy;
import com.lody.virtual.client.hook.base.MethodInvocationStub;
import com.lody.virtual.client.hook.delegate.AppInstrumentation;
import com.lody.virtual.client.hook.proxies.account.AccountManagerStub;
import com.lody.virtual.client.hook.proxies.alarm.AlarmManagerStub;
import com.lody.virtual.client.hook.proxies.am.ActivityManagerStub;
import com.lody.virtual.client.hook.proxies.am.HCallbackStub;
import com.lody.virtual.client.hook.proxies.appops.AppOpsManagerStub;
import com.lody.virtual.client.hook.proxies.appwidget.AppWidgetManagerStub;
import com.lody.virtual.client.hook.proxies.audio.AudioManagerStub;
import com.lody.virtual.client.hook.proxies.backup.BackupManagerStub;
import com.lody.virtual.client.hook.proxies.bluetooth.BluetoothStub;
import com.lody.virtual.client.hook.proxies.clipboard.ClipBoardStub;
import com.lody.virtual.client.hook.proxies.connectivity.ConnectivityStub;
import com.lody.virtual.client.hook.proxies.content.ContentServiceStub;
import com.lody.virtual.client.hook.proxies.context_hub.ContextHubServiceStub;
import com.lody.virtual.client.hook.proxies.display.DisplayStub;
import com.lody.virtual.client.hook.proxies.dropbox.DropBoxManagerStub;
import com.lody.virtual.client.hook.proxies.graphics.GraphicsStatsStub;
import com.lody.virtual.client.hook.proxies.imms.MmsStub;
import com.lody.virtual.client.hook.proxies.input.InputMethodManagerStub;
import com.lody.virtual.client.hook.proxies.isms.ISmsStub;
import com.lody.virtual.client.hook.proxies.isub.ISubStub;
import com.lody.virtual.client.hook.proxies.job.JobServiceStub;
import com.lody.virtual.client.hook.proxies.libcore.LibCoreStub;
import com.lody.virtual.client.hook.proxies.location.LocationManagerStub;
import com.lody.virtual.client.hook.proxies.media.router.MediaRouterServiceStub;
import com.lody.virtual.client.hook.proxies.media.session.SessionManagerStub;
import com.lody.virtual.client.hook.proxies.mount.MountServiceStub;
import com.lody.virtual.client.hook.proxies.network.NetworkManagementStub;
import com.lody.virtual.client.hook.proxies.notification.NotificationManagerStub;
import com.lody.virtual.client.hook.proxies.persistent_data_block.PersistentDataBlockServiceStub;
import com.lody.virtual.client.hook.proxies.phonesubinfo.PhoneSubInfoStub;
import com.lody.virtual.client.hook.proxies.pm.PackageManagerStub;
import com.lody.virtual.client.hook.proxies.power.PowerManagerStub;
import com.lody.virtual.client.hook.proxies.restriction.RestrictionStub;
import com.lody.virtual.client.hook.proxies.search.SearchManagerStub;
import com.lody.virtual.client.hook.proxies.shortcut.ShortcutServiceStub;
import com.lody.virtual.client.hook.proxies.telephony.TelephonyRegistryStub;
import com.lody.virtual.client.hook.proxies.telephony.TelephonyStub;
import com.lody.virtual.client.hook.proxies.user.UserManagerStub;
import com.lody.virtual.client.hook.proxies.vibrator.VibratorStub;
import com.lody.virtual.client.hook.proxies.wifi.WifiManagerStub;
import com.lody.virtual.client.hook.proxies.wifi_scanner.WifiScannerStub;
import com.lody.virtual.client.hook.proxies.window.WindowManagerStub;
import com.lody.virtual.client.interfaces.IInjector;
import java.util.HashMap;
import java.util.Map;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
/**
* @author Lody
*
*/
public final class InvocationStubManager {
private static InvocationStubManager sInstance = new InvocationStubManager();
private static boolean sInit;
private Map, IInjector> mInjectors = new HashMap<>(13);
private InvocationStubManager() {
}
public static InvocationStubManager getInstance() {
return sInstance;
}
void injectAll() throws Throwable {
for (IInjector injector : mInjectors.values()) {
injector.inject();
}
// XXX: Lazy inject the Instrumentation,
addInjector(AppInstrumentation.getDefault());
}
/**
* @return if the InvocationStubManager has been initialized.
*/
public boolean isInit() {
return sInit;
}
public void init() throws Throwable {
if (isInit()) {
throw new IllegalStateException("InvocationStubManager Has been initialized.");
}
injectInternal();
sInit = true;
}
private void injectInternal() throws Throwable {
// VA 自身的 App 进程不需要 Hook
if (VirtualCore.get().isMainProcess()) {
return;
}
// VAService 需要 Hook AMS 和 PMS
if (VirtualCore.get().isServerProcess()) {
addInjector(new ActivityManagerStub());
addInjector(new PackageManagerStub());
return;
}
// Client APP 需要 Hook 整个 framework,来使其调用到 VA framework
if (VirtualCore.get().isVAppProcess()) {
addInjector(new LibCoreStub());
addInjector(new ActivityManagerStub());
addInjector(new PackageManagerStub());
addInjector(HCallbackStub.getDefault());
addInjector(new ISmsStub());
addInjector(new ISubStub());
addInjector(new DropBoxManagerStub());
addInjector(new NotificationManagerStub());
addInjector(new LocationManagerStub());
addInjector(new WindowManagerStub());
addInjector(new ClipBoardStub());
addInjector(new MountServiceStub());
addInjector(new BackupManagerStub());
addInjector(new TelephonyStub());
addInjector(new TelephonyRegistryStub());
addInjector(new PhoneSubInfoStub());
addInjector(new PowerManagerStub());
addInjector(new AppWidgetManagerStub());
addInjector(new AccountManagerStub());
addInjector(new AudioManagerStub());
addInjector(new SearchManagerStub());
addInjector(new ContentServiceStub());
addInjector(new ConnectivityStub());
if (Build.VERSION.SDK_INT >= JELLY_BEAN_MR2) {
addInjector(new VibratorStub());
addInjector(new WifiManagerStub());
addInjector(new BluetoothStub());
addInjector(new ContextHubServiceStub());
}
if (Build.VERSION.SDK_INT >= JELLY_BEAN_MR1) {
addInjector(new UserManagerStub());
}
if (Build.VERSION.SDK_INT >= JELLY_BEAN_MR1) {
addInjector(new DisplayStub());
}
if (Build.VERSION.SDK_INT >= LOLLIPOP) {
addInjector(new PersistentDataBlockServiceStub());
addInjector(new InputMethodManagerStub());
addInjector(new MmsStub());
addInjector(new SessionManagerStub());
addInjector(new JobServiceStub());
addInjector(new RestrictionStub());
}
if (Build.VERSION.SDK_INT >= KITKAT) {
addInjector(new AlarmManagerStub());
addInjector(new AppOpsManagerStub());
addInjector(new MediaRouterServiceStub());
}
if (Build.VERSION.SDK_INT >= LOLLIPOP_MR1) {
addInjector(new GraphicsStatsStub());
}
if (Build.VERSION.SDK_INT >= M) {
addInjector(new NetworkManagementStub());
}
if (Build.VERSION.SDK_INT >= N) {
addInjector(new WifiScannerStub());
addInjector(new ShortcutServiceStub());
}
}
}
private void addInjector(IInjector IInjector) {
mInjectors.put(IInjector.getClass(), IInjector);
}
public T findInjector(Class clazz) {
// noinspection unchecked
return (T) mInjectors.get(clazz);
}
public void checkEnv(Class clazz) {
IInjector IInjector = findInjector(clazz);
if (IInjector != null && IInjector.isEnvBad()) {
try {
IInjector.inject();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
public H getInvocationStub(Class injectorClass) {
T injector = findInjector(injectorClass);
if (injector != null && injector instanceof MethodInvocationProxy) {
// noinspection unchecked
return (H) ((MethodInvocationProxy) injector).getInvocationStub();
}
return null;
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/core/VirtualCore.java
================================================
package com.lody.virtual.client.core;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.Looper;
import android.os.Process;
import android.os.RemoteException;
import android.util.Log;
import com.lody.virtual.R;
import com.lody.virtual.client.VClientImpl;
import com.lody.virtual.client.env.Constants;
import com.lody.virtual.client.env.VirtualRuntime;
import com.lody.virtual.client.fixer.ContextFixer;
import com.lody.virtual.client.hook.delegate.ComponentDelegate;
import com.lody.virtual.client.hook.delegate.PhoneInfoDelegate;
import com.lody.virtual.client.hook.delegate.TaskDescriptionDelegate;
import com.lody.virtual.client.ipc.LocalProxyUtils;
import com.lody.virtual.client.ipc.ServiceManagerNative;
import com.lody.virtual.client.ipc.VActivityManager;
import com.lody.virtual.client.ipc.VPackageManager;
import com.lody.virtual.client.stub.VASettings;
import com.lody.virtual.helper.compat.BundleCompat;
import com.lody.virtual.helper.utils.BitmapUtils;
import com.lody.virtual.os.VUserHandle;
import com.lody.virtual.remote.InstallResult;
import com.lody.virtual.remote.InstalledAppInfo;
import com.lody.virtual.server.IAppManager;
import com.lody.virtual.server.interfaces.IAppRequestListener;
import com.lody.virtual.server.interfaces.IPackageObserver;
import com.lody.virtual.server.interfaces.IUiCallback;
import java.io.IOException;
import java.util.List;
import dalvik.system.DexFile;
import mirror.android.app.ActivityThread;
/**
* @author Lody
* @version 3.5
*/
public final class VirtualCore {
public static final int GET_HIDDEN_APP = 0x00000001;
@SuppressLint("StaticFieldLeak")
private static VirtualCore gCore = new VirtualCore();
private final int myUid = Process.myUid();
/**
* Client Package Manager
*/
private PackageManager unHookPackageManager;
/**
* Host package name
*/
private String hostPkgName;
/**
* ActivityThread instance
*/
private Object mainThread;
private Context context;
/**
* Main ProcessName
*/
private String mainProcessName;
/**
* Real Process Name
*/
private String processName;
private ProcessType processType;
private IAppManager mService;
private boolean isStartUp;
private PackageInfo hostPkgInfo;
private int systemPid;
private ConditionVariable initLock = new ConditionVariable();
private PhoneInfoDelegate phoneInfoDelegate;
private ComponentDelegate componentDelegate;
private TaskDescriptionDelegate taskDescriptionDelegate;
private VirtualCore() {
}
public static VirtualCore get() {
return gCore;
}
public static PackageManager getPM() {
return get().getPackageManager();
}
public static Object mainThread() {
return get().mainThread;
}
public ConditionVariable getInitLock() {
return initLock;
}
public int myUid() {
return myUid;
}
public int myUserId() {
return VUserHandle.getUserId(myUid);
}
public ComponentDelegate getComponentDelegate() {
return componentDelegate == null ? ComponentDelegate.EMPTY : componentDelegate;
}
public void setComponentDelegate(ComponentDelegate delegate) {
this.componentDelegate = delegate;
}
public PhoneInfoDelegate getPhoneInfoDelegate() {
return phoneInfoDelegate;
}
public void setPhoneInfoDelegate(PhoneInfoDelegate phoneInfoDelegate) {
this.phoneInfoDelegate = phoneInfoDelegate;
}
public void setCrashHandler(CrashHandler handler) {
VClientImpl.get().setCrashHandler(handler);
}
public TaskDescriptionDelegate getTaskDescriptionDelegate() {
return taskDescriptionDelegate;
}
public void setTaskDescriptionDelegate(TaskDescriptionDelegate taskDescriptionDelegate) {
this.taskDescriptionDelegate = taskDescriptionDelegate;
}
public int[] getGids() {
return hostPkgInfo.gids;
}
public Context getContext() {
return context;
}
public PackageManager getPackageManager() {
return context.getPackageManager();
}
public String getHostPkg() {
return hostPkgName;
}
public PackageManager getUnHookPackageManager() {
return unHookPackageManager;
}
public void startup(Context context) throws Throwable {
if (!isStartUp) {
// 确保 MainThread
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("VirtualCore.startup() must called in main thread.");
}
VASettings.STUB_CP_AUTHORITY = context.getPackageName() + "." + VASettings.STUB_DEF_AUTHORITY;
ServiceManagerNative.SERVICE_CP_AUTH = context.getPackageName() + "." + ServiceManagerNative.SERVICE_DEF_AUTH;
this.context = context;
// 获取 ActivityThread 实例
mainThread = ActivityThread.currentActivityThread.call();
unHookPackageManager = context.getPackageManager();
hostPkgInfo = unHookPackageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PROVIDERS);
detectProcessType();
// hook 系统类
InvocationStubManager invocationStubManager = InvocationStubManager.getInstance();
invocationStubManager.init();
invocationStubManager.injectAll();
// 修复权限管理
ContextFixer.fixContext(context);
isStartUp = true;
if (initLock != null) {
initLock.open();
initLock = null;
}
}
}
public void waitForEngine() {
ServiceManagerNative.ensureServerStarted();
}
public boolean isEngineLaunched() {
String engineProcessName = getEngineProcessName();
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo info : am.getRunningAppProcesses()) {
if (info.processName.endsWith(engineProcessName)) {
return true;
}
}
return false;
}
public String getEngineProcessName() {
return context.getString(R.string.engine_process_name);
}
public void initialize(VirtualInitializer initializer) {
if (initializer == null) {
throw new IllegalStateException("Initializer = NULL");
}
switch (processType) {
case Main:
initializer.onMainProcess();
break;
case VAppClient:
initializer.onVirtualProcess();
break;
case Server:
initializer.onServerProcess();
break;
case CHILD:
initializer.onChildProcess();
break;
}
}
private void detectProcessType() {
// Host package name
hostPkgName = context.getApplicationInfo().packageName;
// Main process name
mainProcessName = context.getApplicationInfo().processName;
// Current process name
processName = ActivityThread.getProcessName.call(mainThread);
if (processName.equals(mainProcessName)) {
processType = ProcessType.Main;
} else if (processName.endsWith(Constants.SERVER_PROCESS_NAME)) {
processType = ProcessType.Server;
} else if (VActivityManager.get().isAppProcess(processName)) {
processType = ProcessType.VAppClient;
} else {
processType = ProcessType.CHILD;
}
if (isVAppProcess()) {
systemPid = VActivityManager.get().getSystemPid();
}
}
private IAppManager getService() {
if (mService == null
|| (!VirtualCore.get().isVAppProcess() && !mService.asBinder().isBinderAlive())) {
synchronized (this) {
Object remote = getStubInterface();
mService = LocalProxyUtils.genProxy(IAppManager.class, remote);
}
}
return mService;
}
private Object getStubInterface() {
return IAppManager.Stub
.asInterface(ServiceManagerNative.getService(ServiceManagerNative.APP));
}
/**
* @return If the current process is used to VA.
*/
public boolean isVAppProcess() {
return ProcessType.VAppClient == processType;
}
/**
* @return If the current process is the main.
*/
public boolean isMainProcess() {
return ProcessType.Main == processType;
}
/**
* @return If the current process is the child.
*/
public boolean isChildProcess() {
return ProcessType.CHILD == processType;
}
/**
* @return If the current process is the server.
*/
public boolean isServerProcess() {
return ProcessType.Server == processType;
}
/**
* @return the actual process name
*/
public String getProcessName() {
return processName;
}
/**
* @return the Main process name
*/
public String getMainProcessName() {
return mainProcessName;
}
/**
* Optimize the Dalvik-Cache for the specified package.
*
* @param pkg package name
* @throws IOException
*/
public void preOpt(String pkg) throws IOException {
InstalledAppInfo info = getInstalledAppInfo(pkg, 0);
if (info != null && !info.dependSystem && !info.skipDexOpt) {
DexFile.loadDex(info.apkPath, info.getOdexFile().getPath(), 0).close();
}
}
/**
* Is the specified app running in foreground / background?
*
* @param packageName package name
* @param userId user id
* @return if the specified app running in foreground / background.
*/
public boolean isAppRunning(String packageName, int userId) {
return VActivityManager.get().isAppRunning(packageName, userId);
}
public InstallResult installPackage(String apkPath, int flags) {
try {
// 调用远程 VAService
return getService().installPackage(apkPath, flags);
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
public void addVisibleOutsidePackage(String pkg) {
try {
getService().addVisibleOutsidePackage(pkg);
} catch (RemoteException e) {
VirtualRuntime.crash(e);
}
}
public void removeVisibleOutsidePackage(String pkg) {
try {
getService().removeVisibleOutsidePackage(pkg);
} catch (RemoteException e) {
VirtualRuntime.crash(e);
}
}
public boolean isOutsidePackageVisible(String pkg) {
try {
return getService().isOutsidePackageVisible(pkg);
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
public boolean isAppInstalled(String pkg) {
try {
return getService().isAppInstalled(pkg);
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
public boolean isPackageLaunchable(String packageName) {
InstalledAppInfo info = getInstalledAppInfo(packageName, 0);
return info != null
&& getLaunchIntent(packageName, info.getInstalledUsers()[0]) != null;
}
public Intent getLaunchIntent(String packageName, int userId) {
VPackageManager pm = VPackageManager.get();
Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
intentToResolve.addCategory(Intent.CATEGORY_INFO);
intentToResolve.setPackage(packageName);
List ris = pm.queryIntentActivities(intentToResolve, intentToResolve.resolveType(context), 0, userId);
// Otherwise, try to find a main launcher activity.
if (ris == null || ris.size() <= 0) {
// reuse the intent instance
intentToResolve.removeCategory(Intent.CATEGORY_INFO);
intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
intentToResolve.setPackage(packageName);
ris = pm.queryIntentActivities(intentToResolve, intentToResolve.resolveType(context), 0, userId);
}
if (ris == null || ris.size() <= 0) {
return null;
}
Intent intent = new Intent(intentToResolve);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClassName(ris.get(0).activityInfo.packageName,
ris.get(0).activityInfo.name);
return intent;
}
public boolean createShortcut(int userId, String packageName, OnEmitShortcutListener listener) {
return createShortcut(userId, packageName, null, listener);
}
public boolean createShortcut(int userId, String packageName, Intent splash, OnEmitShortcutListener listener) {
InstalledAppInfo setting = getInstalledAppInfo(packageName, 0);
if (setting == null) {
return false;
}
ApplicationInfo appInfo = setting.getApplicationInfo(userId);
PackageManager pm = context.getPackageManager();
String name;
Bitmap icon;
try {
CharSequence sequence = appInfo.loadLabel(pm);
name = sequence.toString();
icon = BitmapUtils.drawableToBitmap(appInfo.loadIcon(pm));
} catch (Throwable e) {
return false;
}
if (listener != null) {
String newName = listener.getName(name);
if (newName != null) {
name = newName;
}
Bitmap newIcon = listener.getIcon(icon);
if (newIcon != null) {
icon = newIcon;
}
}
Intent targetIntent = getLaunchIntent(packageName, userId);
if (targetIntent == null) {
return false;
}
Intent shortcutIntent = new Intent();
shortcutIntent.setClassName(getHostPkg(), Constants.SHORTCUT_PROXY_ACTIVITY_NAME);
shortcutIntent.addCategory(Intent.CATEGORY_DEFAULT);
if (splash != null) {
shortcutIntent.putExtra("_VA_|_splash_", splash.toUri(0));
}
shortcutIntent.putExtra("_VA_|_intent_", targetIntent);
shortcutIntent.putExtra("_VA_|_uri_", targetIntent.toUri(0));
shortcutIntent.putExtra("_VA_|_user_id_", VUserHandle.myUserId());
Intent addIntent = new Intent();
addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
addIntent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
context.sendBroadcast(addIntent);
return true;
}
public boolean removeShortcut(int userId, String packageName, Intent splash, OnEmitShortcutListener listener) {
InstalledAppInfo setting = getInstalledAppInfo(packageName, 0);
if (setting == null) {
return false;
}
ApplicationInfo appInfo = setting.getApplicationInfo(userId);
PackageManager pm = context.getPackageManager();
String name;
try {
CharSequence sequence = appInfo.loadLabel(pm);
name = sequence.toString();
} catch (Throwable e) {
return false;
}
if (listener != null) {
String newName = listener.getName(name);
if (newName != null) {
name = newName;
}
}
Intent targetIntent = getLaunchIntent(packageName, userId);
if (targetIntent == null) {
return false;
}
Intent shortcutIntent = new Intent();
shortcutIntent.setClassName(getHostPkg(), Constants.SHORTCUT_PROXY_ACTIVITY_NAME);
shortcutIntent.addCategory(Intent.CATEGORY_DEFAULT);
if (splash != null) {
shortcutIntent.putExtra("_VA_|_splash_", splash.toUri(0));
}
shortcutIntent.putExtra("_VA_|_intent_", targetIntent);
shortcutIntent.putExtra("_VA_|_uri_", targetIntent.toUri(0));
shortcutIntent.putExtra("_VA_|_user_id_", VUserHandle.myUserId());
Intent addIntent = new Intent();
addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
addIntent.setAction("com.android.launcher.action.UNINSTALL_SHORTCUT");
context.sendBroadcast(addIntent);
return true;
}
public abstract static class UiCallback extends IUiCallback.Stub {
}
public void setUiCallback(Intent intent, IUiCallback callback) {
if (callback != null) {
Bundle bundle = new Bundle();
BundleCompat.putBinder(bundle, "_VA_|_ui_callback_", callback.asBinder());
intent.putExtra("_VA_|_sender_", bundle);
}
}
public InstalledAppInfo getInstalledAppInfo(String pkg, int flags) {
try {
return getService().getInstalledAppInfo(pkg, flags);
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
public int getInstalledAppCount() {
try {
return getService().getInstalledAppCount();
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
public boolean isStartup() {
return isStartUp;
}
public boolean uninstallPackageAsUser(String pkgName, int userId) {
try {
return getService().uninstallPackageAsUser(pkgName, userId);
} catch (RemoteException e) {
// Ignore
}
return false;
}
public boolean uninstallPackage(String pkgName) {
try {
return getService().uninstallPackage(pkgName);
} catch (RemoteException e) {
// Ignore
}
return false;
}
public Resources getResources(String pkg) throws Resources.NotFoundException {
InstalledAppInfo installedAppInfo = getInstalledAppInfo(pkg, 0);
if (installedAppInfo != null) {
AssetManager assets = mirror.android.content.res.AssetManager.ctor.newInstance();
mirror.android.content.res.AssetManager.addAssetPath.call(assets, installedAppInfo.apkPath);
Resources hostRes = context.getResources();
return new Resources(assets, hostRes.getDisplayMetrics(), hostRes.getConfiguration());
}
throw new Resources.NotFoundException(pkg);
}
public synchronized ActivityInfo resolveActivityInfo(Intent intent, int userId) {
ActivityInfo activityInfo = null;
if (intent.getComponent() == null) {
ResolveInfo resolveInfo = VPackageManager.get().resolveIntent(intent, intent.getType(), 0, userId);
if (resolveInfo != null && resolveInfo.activityInfo != null) {
activityInfo = resolveInfo.activityInfo;
intent.setClassName(activityInfo.packageName, activityInfo.name);
}
} else {
activityInfo = resolveActivityInfo(intent.getComponent(), userId);
}
if (activityInfo != null) {
if (activityInfo.targetActivity != null) {
ComponentName componentName = new ComponentName(activityInfo.packageName, activityInfo.targetActivity);
activityInfo = VPackageManager.get().getActivityInfo(componentName, 0, userId);
intent.setComponent(componentName);
}
}
return activityInfo;
}
public ActivityInfo resolveActivityInfo(ComponentName componentName, int userId) {
return VPackageManager.get().getActivityInfo(componentName, 0, userId);
}
public ServiceInfo resolveServiceInfo(Intent intent, int userId) {
ServiceInfo serviceInfo = null;
ResolveInfo resolveInfo = VPackageManager.get().resolveService(intent, intent.getType(), 0, userId);
if (resolveInfo != null) {
serviceInfo = resolveInfo.serviceInfo;
}
return serviceInfo;
}
public void killApp(String pkg, int userId) {
VActivityManager.get().killAppByPkg(pkg, userId);
}
public void killAllApps() {
VActivityManager.get().killAllApps();
}
public List getInstalledApps(int flags) {
try {
return getService().getInstalledApps(flags);
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
public List getInstalledAppsAsUser(int userId, int flags) {
try {
return getService().getInstalledAppsAsUser(userId, flags);
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
public void clearAppRequestListener() {
try {
getService().clearAppRequestListener();
} catch (RemoteException e) {
e.printStackTrace();
}
}
public void scanApps() {
try {
getService().scanApps();
} catch (RemoteException e) {
// Ignore
}
}
public IAppRequestListener getAppRequestListener() {
try {
return getService().getAppRequestListener();
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
public void setAppRequestListener(final AppRequestListener listener) {
IAppRequestListener inner = new IAppRequestListener.Stub() {
@Override
public void onRequestInstall(final String path) {
VirtualRuntime.getUIHandler().post(new Runnable() {
@Override
public void run() {
listener.onRequestInstall(path);
}
});
}
@Override
public void onRequestUninstall(final String pkg) {
VirtualRuntime.getUIHandler().post(new Runnable() {
@Override
public void run() {
listener.onRequestUninstall(pkg);
}
});
}
};
try {
getService().setAppRequestListener(inner);
} catch (RemoteException e) {
e.printStackTrace();
}
}
public boolean isPackageLaunched(int userId, String packageName) {
try {
return getService().isPackageLaunched(userId, packageName);
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
public void setPackageHidden(int userId, String packageName, boolean hidden) {
try {
getService().setPackageHidden(userId, packageName, hidden);
} catch (RemoteException e) {
e.printStackTrace();
}
}
public boolean installPackageAsUser(int userId, String packageName) {
try {
return getService().installPackageAsUser(userId, packageName);
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
public boolean isAppInstalledAsUser(int userId, String packageName) {
try {
return getService().isAppInstalledAsUser(userId, packageName);
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
public int[] getPackageInstalledUsers(String packageName) {
try {
return getService().getPackageInstalledUsers(packageName);
} catch (RemoteException e) {
return VirtualRuntime.crash(e);
}
}
public abstract static class PackageObserver extends IPackageObserver.Stub {
}
public void registerObserver(IPackageObserver observer) {
try {
getService().registerObserver(observer);
} catch (RemoteException e) {
VirtualRuntime.crash(e);
}
}
public void unregisterObserver(IPackageObserver observer) {
try {
getService().unregisterObserver(observer);
} catch (RemoteException e) {
VirtualRuntime.crash(e);
}
}
// 是否在系统中安装
public boolean isOutsideInstalled(String packageName) {
try {
return unHookPackageManager.getApplicationInfo(packageName, 0) != null;
} catch (PackageManager.NameNotFoundException e) {
// Ignore
}
return false;
}
public int getSystemPid() {
return systemPid;
}
/**
* Process type
*/
private enum ProcessType {
/**
* Server process
*/
Server,
/**
* Virtual app process
*/
VAppClient,
/**
* Main process
*/
Main,
/**
* Child process
*/
CHILD
}
public interface AppRequestListener {
void onRequestInstall(String path);
void onRequestUninstall(String pkg);
}
public interface OnEmitShortcutListener {
Bitmap getIcon(Bitmap originIcon);
String getName(String originName);
}
public static abstract class VirtualInitializer {
public void onMainProcess() {
}
public void onVirtualProcess() {
}
public void onServerProcess() {
}
public void onChildProcess() {
}
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/env/Constants.java
================================================
package com.lody.virtual.client.env;
import android.app.PendingIntent;
import android.content.Intent;
import com.lody.virtual.client.stub.ShortcutHandleActivity;
/**
* @author Lody
*
*/
public class Constants {
public static final String EXTRA_USER_HANDLE = "android.intent.extra.user_handle";
/**
* If an apk declared the "fake-signature" attribute on its Application TAG,
* we will use its signature instead of the real signature.
*
* For more detail, please see :
* https://github.com/microg/android_packages_apps_GmsCore/blob/master/
* patches/android_frameworks_base-M.patch.
*/
public static final String FEATURE_FAKE_SIGNATURE = "fake-signature";
public static final String ACTION_PACKAGE_ADDED = "virtual." + Intent.ACTION_PACKAGE_ADDED;
public static final String ACTION_PACKAGE_REMOVED = "virtual." + Intent.ACTION_PACKAGE_REMOVED;
public static final String ACTION_PACKAGE_CHANGED = "virtual." + Intent.ACTION_PACKAGE_CHANGED;
public static final String ACTION_USER_ADDED = "virtual." + "android.intent.action.USER_ADDED";
public static final String ACTION_USER_REMOVED = "virtual." + "android.intent.action.USER_REMOVED";
public static final String ACTION_USER_INFO_CHANGED = "virtual." + "android.intent.action.USER_CHANGED";
public static final String ACTION_USER_STARTED = "Virtual." + "android.intent.action.USER_STARTED";
public static String META_KEY_IDENTITY = "X-Identity";
public static String META_VALUE_STUB = "Stub-User";
/**
* Server process name of VA
*/
public static String SERVER_PROCESS_NAME = ":x";
/**
* The activity who handle the shortcut.
*/
public static String SHORTCUT_PROXY_ACTIVITY_NAME = ShortcutHandleActivity.class.getName();
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/env/DeadServerException.java
================================================
package com.lody.virtual.client.env;
/**
* @author Lody
*/
public class DeadServerException extends RuntimeException {
public DeadServerException() {
}
public DeadServerException(String message) {
super(message);
}
public DeadServerException(String message, Throwable cause) {
super(message, cause);
}
public DeadServerException(Throwable cause) {
super(cause);
}
public DeadServerException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/env/SpecialComponentList.java
================================================
package com.lody.virtual.client.env;
import android.Manifest;
import android.app.DownloadManager;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import mirror.android.webkit.IWebViewUpdateService;
import mirror.android.webkit.WebViewFactory;
/**
* @author Lody
*/
public final class SpecialComponentList {
private static final List ACTION_BLACK_LIST = new ArrayList(1);
private static final Map PROTECTED_ACTION_MAP = new HashMap<>(5);
private static final HashSet WHITE_PERMISSION = new HashSet<>(3);
// 加密
private static final HashSet INSTRUMENTATION_CONFLICTING = new HashSet<>(2);
private static final HashSet SPEC_SYSTEM_APP_LIST = new HashSet<>(3);
private static final Set SYSTEM_BROADCAST_ACTION = new HashSet<>(7);
private static String PROTECT_ACTION_PREFIX = "_VA_protected_";
static {
SYSTEM_BROADCAST_ACTION.add(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
SYSTEM_BROADCAST_ACTION.add(Intent.ACTION_SCREEN_ON);
SYSTEM_BROADCAST_ACTION.add(Intent.ACTION_SCREEN_OFF);
SYSTEM_BROADCAST_ACTION.add(Intent.ACTION_NEW_OUTGOING_CALL);
SYSTEM_BROADCAST_ACTION.add(Intent.ACTION_TIME_TICK);
SYSTEM_BROADCAST_ACTION.add(Intent.ACTION_TIME_CHANGED);
SYSTEM_BROADCAST_ACTION.add(Intent.ACTION_TIMEZONE_CHANGED);
SYSTEM_BROADCAST_ACTION.add(Intent.ACTION_BATTERY_CHANGED);
SYSTEM_BROADCAST_ACTION.add(Intent.ACTION_BATTERY_LOW);
SYSTEM_BROADCAST_ACTION.add(Intent.ACTION_BATTERY_OKAY);
SYSTEM_BROADCAST_ACTION.add(Intent.ACTION_POWER_CONNECTED);
SYSTEM_BROADCAST_ACTION.add(Intent.ACTION_POWER_DISCONNECTED);
SYSTEM_BROADCAST_ACTION.add("android.provider.Telephony.SMS_RECEIVED");
SYSTEM_BROADCAST_ACTION.add("android.provider.Telephony.SMS_DELIVER");
SYSTEM_BROADCAST_ACTION.add("android.net.wifi.STATE_CHANGE");
SYSTEM_BROADCAST_ACTION.add("android.net.wifi.SCAN_RESULTS");
SYSTEM_BROADCAST_ACTION.add("android.net.wifi.WIFI_STATE_CHANGED");
SYSTEM_BROADCAST_ACTION.add("android.net.conn.CONNECTIVITY_CHANGE");
SYSTEM_BROADCAST_ACTION.add("android.intent.action.ANY_DATA_STATE");
SYSTEM_BROADCAST_ACTION.add("android.intent.action.SIM_STATE_CHANGED");
SYSTEM_BROADCAST_ACTION.add("android.location.PROVIDERS_CHANGED");
SYSTEM_BROADCAST_ACTION.add("android.location.MODE_CHANGED");
ACTION_BLACK_LIST.add("android.appwidget.action.APPWIDGET_UPDATE");
WHITE_PERMISSION.add("com.google.android.gms.settings.SECURITY_SETTINGS");
WHITE_PERMISSION.add("com.google.android.apps.plus.PRIVACY_SETTINGS");
WHITE_PERMISSION.add(Manifest.permission.ACCOUNT_MANAGER);
PROTECTED_ACTION_MAP.put(Intent.ACTION_PACKAGE_ADDED, Constants.ACTION_PACKAGE_ADDED);
PROTECTED_ACTION_MAP.put(Intent.ACTION_PACKAGE_REMOVED, Constants.ACTION_PACKAGE_REMOVED);
PROTECTED_ACTION_MAP.put(Intent.ACTION_PACKAGE_CHANGED, Constants.ACTION_PACKAGE_CHANGED);
PROTECTED_ACTION_MAP.put("android.intent.action.USER_ADDED", Constants.ACTION_USER_ADDED);
PROTECTED_ACTION_MAP.put("android.intent.action.USER_REMOVED", Constants.ACTION_USER_REMOVED);
PROTECTED_ACTION_MAP.put("com.gy.test","com.gy.test");
INSTRUMENTATION_CONFLICTING.add("com.qihoo.magic");
INSTRUMENTATION_CONFLICTING.add("com.qihoo.magic_mutiple");
INSTRUMENTATION_CONFLICTING.add("com.facebook.katana");
SPEC_SYSTEM_APP_LIST.add("android");
SPEC_SYSTEM_APP_LIST.add("com.google.android.webview");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
String webViewPkgN = IWebViewUpdateService.getCurrentWebViewPackageName.call(WebViewFactory.getUpdateService.call());
if (webViewPkgN != null) {
SPEC_SYSTEM_APP_LIST.add(webViewPkgN);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
public static boolean isSpecSystemPackage(String pkg) {
return SPEC_SYSTEM_APP_LIST.contains(pkg);
}
public static boolean isConflictingInstrumentation(String packageName) {
return INSTRUMENTATION_CONFLICTING.contains(packageName);
}
/**
* Check if the action in the BlackList.
*
* @param action Action
*/
public static boolean isActionInBlackList(String action) {
return ACTION_BLACK_LIST.contains(action);
}
/**
* Add an action to the BlackList.
*
* @param action action
*/
public static void addBlackAction(String action) {
ACTION_BLACK_LIST.add(action);
}
public static void protectIntentFilter(IntentFilter filter) {
if (filter != null) {
List actions = mirror.android.content.IntentFilter.mActions.get(filter);
ListIterator iterator = actions.listIterator();
while (iterator.hasNext()) {
String action = iterator.next();
if (SpecialComponentList.isActionInBlackList(action)) {
iterator.remove();
continue;
}
if (SYSTEM_BROADCAST_ACTION.contains(action)) {
continue;
}
String newAction = SpecialComponentList.protectAction(action);
if (newAction != null) {
iterator.set(newAction);
}
}
}
}
public static void protectIntent(Intent intent) {
String protectAction = protectAction(intent.getAction());
if (protectAction != null) {
intent.setAction(protectAction);
}
}
public static void unprotectIntent(Intent intent) {
String unprotectAction = unprotectAction(intent.getAction());
if (unprotectAction != null) {
intent.setAction(unprotectAction);
}
}
public static String protectAction(String originAction) {
if (originAction == null) {
return null;
}
if (originAction.startsWith("_VA_")) {
return originAction;
}
String newAction = PROTECTED_ACTION_MAP.get(originAction);
if (newAction == null) {
newAction = PROTECT_ACTION_PREFIX + originAction;
}
return newAction;
}
public static String unprotectAction(String action) {
if (action == null) {
return null;
}
if (action.startsWith(PROTECT_ACTION_PREFIX)) {
return action.substring(PROTECT_ACTION_PREFIX.length());
}
for (Map.Entry next : PROTECTED_ACTION_MAP.entrySet()) {
String modifiedAction = next.getValue();
if (modifiedAction.equals(action)) {
return next.getKey();
}
}
return null;
}
public static boolean isWhitePermission(String permission) {
return WHITE_PERMISSION.contains(permission);
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/env/VirtualRuntime.java
================================================
package com.lody.virtual.client.env;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.RemoteException;
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.helper.compat.ApplicationThreadCompat;
import com.lody.virtual.helper.utils.VLog;
import mirror.android.ddm.DdmHandleAppName;
import mirror.android.ddm.DdmHandleAppNameJBMR1;
/**
* @author Lody
*
*
* Runtime Environment for App.
*/
public class VirtualRuntime {
private static final Handler sUIHandler = new Handler(Looper.getMainLooper());
private static String sInitialPackageName;
private static String sProcessName;
public static Handler getUIHandler() {
return sUIHandler;
}
public static String getProcessName() {
return sProcessName;
}
public static String getInitialPackageName() {
return sInitialPackageName;
}
public static void setupRuntime(String processName, ApplicationInfo appInfo) {
if (sProcessName != null) {
return;
}
sInitialPackageName = appInfo.packageName;
sProcessName = processName;
// 设置 process 的名字
mirror.android.os.Process.setArgV0.call(processName);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
DdmHandleAppNameJBMR1.setAppName.call(processName, 0);
} else {
DdmHandleAppName.setAppName.call(processName);
}
}
public static T crash(RemoteException e) throws RuntimeException {
e.printStackTrace();
if (VirtualCore.get().isVAppProcess()) {
Process.killProcess(Process.myPid());
System.exit(0);
}
throw new DeadServerException(e);
}
public static boolean isArt() {
return System.getProperty("java.vm.version").startsWith("2");
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/fixer/ActivityFixer.java
================================================
package com.lody.virtual.client.fixer;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.WallpaperManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import mirror.com.android.internal.R_Hide;
/**
* @author Lody
*
*/
public final class ActivityFixer {
private ActivityFixer() {
}
public static void fixActivity(Activity activity) {
Context baseContext = activity.getBaseContext();
try {
TypedArray typedArray = activity.obtainStyledAttributes((R_Hide.styleable.Window.get()));
if (typedArray != null) {
boolean showWallpaper = typedArray.getBoolean(R_Hide.styleable.Window_windowShowWallpaper.get(),
false);
if (showWallpaper) {
activity.getWindow().setBackgroundDrawable(WallpaperManager.getInstance(activity).getDrawable());
}
typedArray.recycle();
}
} catch (Throwable e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Intent intent = activity.getIntent();
ApplicationInfo applicationInfo = baseContext.getApplicationInfo();
PackageManager pm = activity.getPackageManager();
if (intent != null && activity.isTaskRoot()) {
try {
String label = applicationInfo.loadLabel(pm) + "";
Bitmap icon = null;
Drawable drawable = applicationInfo.loadIcon(pm);
if (drawable instanceof BitmapDrawable) {
icon = ((BitmapDrawable) drawable).getBitmap();
}
activity.setTaskDescription(new ActivityManager.TaskDescription(label, icon));
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/fixer/ComponentFixer.java
================================================
package com.lody.virtual.client.fixer;
import android.content.pm.ComponentInfo;
import android.text.TextUtils;
import com.lody.virtual.server.pm.PackageSetting;
/**
* @author Lody
*/
public class ComponentFixer {
public static String fixComponentClassName(String pkgName, String className) {
if (className != null) {
if (className.charAt(0) == '.') {
return pkgName + className;
}
return className;
}
return null;
}
public static void fixComponentInfo(PackageSetting setting, ComponentInfo info, int userId) {
if (info != null) {
if (TextUtils.isEmpty(info.processName)) {
info.processName = info.packageName;
}
info.name = fixComponentClassName(info.packageName, info.name);
if (info.processName == null) {
info.processName = info.applicationInfo.processName;
}
}
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/fixer/ContextFixer.java
================================================
package com.lody.virtual.client.fixer;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Build;
import android.os.DropBoxManager;
import com.lody.virtual.client.core.InvocationStubManager;
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.client.hook.base.BinderInvocationStub;
import com.lody.virtual.client.hook.proxies.dropbox.DropBoxManagerStub;
import com.lody.virtual.client.hook.proxies.graphics.GraphicsStatsStub;
import com.lody.virtual.helper.utils.Reflect;
import com.lody.virtual.helper.utils.ReflectException;
import mirror.android.app.ContextImpl;
import mirror.android.app.ContextImplKitkat;
import mirror.android.content.ContentResolverJBMR2;
/**
* @author Lody
*/
public class ContextFixer {
private static final String TAG = ContextFixer.class.getSimpleName();
/**
* Fuck AppOps
*
* @param context Context
*/
public static void fixContext(Context context) {
try {
context.getPackageName();
} catch (Throwable e) {
return;
}
InvocationStubManager.getInstance().checkEnv(GraphicsStatsStub.class);
int deep = 0;
while (context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext();
deep++;
if (deep >= 10) {
return;
}
}
ContextImpl.mPackageManager.set(context, null);
try {
context.getPackageManager();
} catch (Throwable e) {
e.printStackTrace();
}
if (!VirtualCore.get().isVAppProcess()) {
return;
}
DropBoxManager dm = (DropBoxManager) context.getSystemService(Context.DROPBOX_SERVICE);
BinderInvocationStub boxBinder = InvocationStubManager.getInstance().getInvocationStub(DropBoxManagerStub.class);
if (boxBinder != null) {
try {
Reflect.on(dm).set("mService", boxBinder.getProxyInterface());
} catch (ReflectException e) {
e.printStackTrace();
}
}
String hostPkg = VirtualCore.get().getHostPkg();
ContextImpl.mBasePackageName.set(context, hostPkg);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
ContextImplKitkat.mOpPackageName.set(context, hostPkg);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
ContentResolverJBMR2.mPackageName.set(context.getContentResolver(), hostPkg);
}
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/hook/base/BinderInvocationProxy.java
================================================
package com.lody.virtual.client.hook.base;
import android.os.IBinder;
import android.os.IInterface;
import mirror.RefStaticMethod;
import mirror.android.os.ServiceManager;
/**
* @author Paulo Costa
*
* @see MethodInvocationProxy
*/
public abstract class BinderInvocationProxy extends MethodInvocationProxy {
protected String mServiceName;
public BinderInvocationProxy(IInterface stub, String serviceName) {
this(new BinderInvocationStub(stub), serviceName);
}
public BinderInvocationProxy(RefStaticMethod asInterfaceMethod, String serviceName) {
this(new BinderInvocationStub(asInterfaceMethod, ServiceManager.getService.call(serviceName)), serviceName);
}
public BinderInvocationProxy(Class> stubClass, String serviceName) {
this(new BinderInvocationStub(stubClass, ServiceManager.getService.call(serviceName)), serviceName);
}
public BinderInvocationProxy(BinderInvocationStub hookDelegate, String serviceName) {
super(hookDelegate);
this.mServiceName = serviceName;
}
@Override
public void inject() throws Throwable {
getInvocationStub().replaceService(mServiceName);
}
@Override
public boolean isEnvBad() {
IBinder binder = ServiceManager.getService.call(mServiceName);
return binder != null && getInvocationStub() != binder;
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/hook/base/BinderInvocationStub.java
================================================
package com.lody.virtual.client.hook.base;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Log;
import com.lody.virtual.client.core.VirtualCore;
import java.io.FileDescriptor;
import java.lang.reflect.Method;
import mirror.RefStaticMethod;
import mirror.android.os.ServiceManager;
/**
* @author Lody
*/
// IBinder hook asInterface 接口
@SuppressWarnings("unchecked")
public class BinderInvocationStub extends MethodInvocationStub implements IBinder {
private static final String TAG = BinderInvocationStub.class.getSimpleName();
private IBinder mBaseBinder;
public BinderInvocationStub(RefStaticMethod asInterfaceMethod, IBinder binder) {
this(asInterface(asInterfaceMethod, binder));
}
public BinderInvocationStub(Class> stubClass, IBinder binder) {
this(asInterface(stubClass, binder));
}
public BinderInvocationStub(IInterface mBaseInterface) {
super(mBaseInterface);
mBaseBinder = getBaseInterface() != null ? getBaseInterface().asBinder() : null;
addMethodProxy(new AsBinder());
}
private static IInterface asInterface(RefStaticMethod asInterfaceMethod, IBinder binder) {
if (asInterfaceMethod == null || binder == null) {
return null;
}
return asInterfaceMethod.call(binder);
}
private static IInterface asInterface(Class> stubClass, IBinder binder) {
try {
if (stubClass == null || binder == null) {
return null;
}
Method asInterface = stubClass.getMethod("asInterface", IBinder.class);
return (IInterface) asInterface.invoke(null, binder);
} catch (Exception e) {
Log.d(TAG, "Could not create stub " + stubClass.getName() + ". Cause: " + e);
return null;
}
}
public void replaceService(String name) {
if (mBaseBinder != null) {
ServiceManager.sCache.get().put(name, this);
}
}
private final class AsBinder extends MethodProxy {
@Override
public String getMethodName() {
return "asBinder";
}
@Override
public Object call(Object who, Method method, Object... args) throws Throwable {
return BinderInvocationStub.this;
}
}
@Override
public String getInterfaceDescriptor() throws RemoteException {
return mBaseBinder.getInterfaceDescriptor();
}
public Context getContext() {
return VirtualCore.get().getContext();
}
@Override
public boolean pingBinder() {
return mBaseBinder.pingBinder();
}
@Override
public boolean isBinderAlive() {
return mBaseBinder.isBinderAlive();
}
@Override
public IInterface queryLocalInterface(String descriptor) {
return getProxyInterface();
}
@Override
public void dump(FileDescriptor fd, String[] args) throws RemoteException {
mBaseBinder.dump(fd, args);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
@Override
public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException {
mBaseBinder.dumpAsync(fd, args);
}
@Override
public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
return mBaseBinder.transact(code, data, reply, flags);
}
@Override
public void linkToDeath(DeathRecipient recipient, int flags) throws RemoteException {
mBaseBinder.linkToDeath(recipient, flags);
}
@Override
public boolean unlinkToDeath(DeathRecipient recipient, int flags) {
return mBaseBinder.unlinkToDeath(recipient, flags);
}
public IBinder getBaseBinder() {
return mBaseBinder;
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/hook/base/Inject.java
================================================
package com.lody.virtual.client.hook.base;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Lody
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
Class> value();
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/hook/base/LogInvocation.java
================================================
package com.lody.virtual.client.hook.base;
import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Add this annotation to a {@link MethodProxy} or a {@link MethodInvocationStub} to
* log all the calls and their arguments.
*
* Obviously, this is only useful for debugging.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface LogInvocation {
public Condition value() default Condition.ALWAYS;
static enum Condition {
/** Never logs anything */
NEVER {
public int getLogLevel(boolean isHooked, boolean isError) {
return -1;
}
},
/**
* Logs every call.
* Mostly useful for debugging.
*/
ALWAYS {
public int getLogLevel(boolean isHooked, boolean isError) {
return isError ? Log.WARN : Log.INFO;
}
},
/**
* Logs only calls that exited with error
* A reasonable tradeoff between noise and getting relevant information
*/
ON_ERROR {
public int getLogLevel(boolean isHooked, boolean isError) {
return isError ? Log.WARN : -1;
}
},
/**
* Log only calls that haven't been hooked
* It only makes sense on a MethodInvocationProxy, and is useful to pinpoint missing methods
*/
NOT_HOOKED {
public int getLogLevel(boolean isHooked, boolean isError) {
return isHooked ? -1 : isError ? Log.WARN : Log.INFO;
}
};
public abstract int getLogLevel(boolean isHooked, boolean isError);
};
};
================================================
FILE: lib/src/main/java/com/lody/virtual/client/hook/base/MethodBox.java
================================================
package com.lody.virtual.client.hook.base;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author Lody
*/
@SuppressWarnings("unchecked")
public class MethodBox {
public final Method method;
public final Object who;
public final Object[] args;
public MethodBox(Method method, Object who, Object[] args) {
this.method = method;
this.who = who;
this.args = args;
}
public T call() throws InvocationTargetException {
try {
return (T) method.invoke(who, args);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public T callSafe() {
try {
return (T) method.invoke(who, args);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/hook/base/MethodInvocationProxy.java
================================================
package com.lody.virtual.client.hook.base;
import android.content.Context;
import com.lody.virtual.client.core.InvocationStubManager;
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.client.interfaces.IInjector;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
/**
* @author Lody
*
* This class is responsible with:
* - Instantiating a {@link MethodInvocationStub.HookInvocationHandler} on {@link #getInvocationStub()} ()}
* - Install a bunch of {@link MethodProxy}s, either with a @{@link Inject} annotation or manually
* calling {@link #addMethodProxy(MethodProxy)} from {@link #onBindMethods()}
* - Install the hooked object on the Runtime via {@link #inject()}
*
* All {@link MethodInvocationProxy}s (plus a couple of other @{@link IInjector}s are installed by
* {@link InvocationStubManager}
* @see Inject
*/
//Method Hook 通过动态代理
public abstract class MethodInvocationProxy implements IInjector {
protected T mInvocationStub;
public MethodInvocationProxy(T invocationStub) {
this.mInvocationStub = invocationStub;
onBindMethods();
afterHookApply(invocationStub);
LogInvocation loggingAnnotation = getClass().getAnnotation(LogInvocation.class);
if (loggingAnnotation != null) {
invocationStub.setInvocationLoggingCondition(loggingAnnotation.value());
}
}
protected void onBindMethods() {
if (mInvocationStub == null) {
return;
}
Class extends MethodInvocationProxy> clazz = getClass();
Inject inject = clazz.getAnnotation(Inject.class);
if (inject != null) {
Class> proxiesClass = inject.value();
Class>[] innerClasses = proxiesClass.getDeclaredClasses();
// 遍历内部类
for (Class> innerClass : innerClasses) {
if (!Modifier.isAbstract(innerClass.getModifiers())
&& MethodProxy.class.isAssignableFrom(innerClass)
&& innerClass.getAnnotation(SkipInject.class) == null) {
addMethodProxy(innerClass);
}
}
}
}
private void addMethodProxy(Class> hookType) {
try {
Constructor> constructor = hookType.getDeclaredConstructors()[0];
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
MethodProxy methodProxy;
if (constructor.getParameterTypes().length == 0) {
methodProxy = (MethodProxy) constructor.newInstance();
} else {
methodProxy = (MethodProxy) constructor.newInstance(this);
}
mInvocationStub.addMethodProxy(methodProxy);
} catch (Throwable e) {
throw new RuntimeException("Unable to instance Hook : " + hookType + " : " + e.getMessage());
}
}
public MethodProxy addMethodProxy(MethodProxy methodProxy) {
return mInvocationStub.addMethodProxy(methodProxy);
}
protected void afterHookApply(T delegate) {
}
@Override
public abstract void inject() throws Throwable;
public Context getContext() {
return VirtualCore.get().getContext();
}
public T getInvocationStub() {
return mInvocationStub;
}
}
================================================
FILE: lib/src/main/java/com/lody/virtual/client/hook/base/MethodInvocationStub.java
================================================
package com.lody.virtual.client.hook.base;
import android.text.TextUtils;
import android.util.Log;
import com.lody.virtual.client.hook.utils.MethodParameterUtils;
import com.lody.virtual.helper.utils.VLog;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* @author Lody
*
* HookHandler uses Java's {@link Proxy} to create a wrapper for existing services.
*
* When any method is called on the wrapper, it checks if there is any {@link MethodProxy} registered
* and enabled for that method. If so, it calls the startUniformer instead of the wrapped implementation.
*