Access purchased paid features on beta builds

Since PCAPdroid 1.5.3, users which purchased paid features in official
builds will be able to use them in beta builds.
This commit is contained in:
emanuele-f 2022-07-09 23:49:22 +02:00
parent c8f61760f8
commit 8446e0962d
6 changed files with 122 additions and 8 deletions

View File

@ -40,6 +40,7 @@ import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
/* Billing stub */
@ -69,6 +70,7 @@ public class Billing {
protected final Context mContext;
protected SharedPreferences mPrefs;
private final HashSet<String> mPeerSkus = new HashSet<>();
protected Billing(Context ctx) {
mContext = ctx;
@ -84,6 +86,9 @@ public class Billing {
}
public boolean isPurchased(String sku) {
if(mPeerSkus.contains(sku))
return true;
return !getLicense().isEmpty();
}
@ -188,4 +193,8 @@ public class Billing {
else
return !Prefs.isRootCaptureEnabled(mPrefs);
}
public void addPeerSku(String sku) {
mPeerSkus.add(sku);
}
}

View File

@ -60,7 +60,7 @@ public class PCAPdroid extends Application {
public void onCreate() {
super.onCreate();
Utils.BuildType buildtp = Utils.getBuildType(this);
Utils.BuildType buildtp = Utils.getVerifiedBuild(this);
Log.d(TAG, "Build type: " + buildtp);
CaocConfig.Builder builder = CaocConfig.Builder.create();

View File

@ -59,7 +59,6 @@ import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.text.Html;
import android.text.SpannableString;
import android.text.SpannedString;
import android.text.TextUtils;
@ -1020,16 +1019,16 @@ public class Utils {
return false;
}
@SuppressWarnings("deprecation")
public static BuildType getBuildType(Context ctx) {
public static BuildType getVerifiedBuild(Context ctx, String package_name) {
try {
Signature[] signatures;
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// NOTE: PCAPdroid does not use multiple signatures
PackageInfo pInfo = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), PackageManager.GET_SIGNING_CERTIFICATES);
PackageInfo pInfo = ctx.getPackageManager().getPackageInfo(package_name, PackageManager.GET_SIGNING_CERTIFICATES);
signatures = (pInfo.signingInfo == null) ? null : pInfo.signingInfo.getSigningCertificateHistory();
} else {
@SuppressLint("PackageManagerGetSignatures")
PackageInfo pInfo = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), PackageManager.GET_SIGNATURES);
signatures = pInfo.signatures;
}
@ -1057,6 +1056,10 @@ public class Utils {
return BuildType.UNKNOWN;
}
public static BuildType getVerifiedBuild(Context ctx) {
return getVerifiedBuild(ctx, ctx.getPackageName());
}
public static X509Certificate x509FromPem(String pem) {
int begin = pem.indexOf('\n') + 1;
int end = pem.indexOf('-', begin);

View File

@ -42,6 +42,7 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.emanuelef.remote_capture.AppsResolver;
import com.emanuelef.remote_capture.Billing;
import com.emanuelef.remote_capture.BuildConfig;
import com.emanuelef.remote_capture.CaptureHelper;
import com.emanuelef.remote_capture.CaptureService;
@ -53,10 +54,13 @@ import com.emanuelef.remote_capture.model.CaptureSettings;
import com.emanuelef.remote_capture.model.CtrlPermissions;
import com.emanuelef.remote_capture.model.CaptureStats;
import java.util.HashSet;
public class CaptureCtrl extends AppCompatActivity {
public static final String ACTION_START = "start";
public static final String ACTION_STOP = "stop";
public static final String ACTION_STATUS = "get_status";
public static final String ACTION_PEER_INFO = "get_peer_info";
public static final String ACTION_NOTIFY_STATUS = "com.emanuelef.remote_capture.CaptureStatus";
private static final String TAG = "CaptureCtrl";
private static AppDescriptor mStarterApp = null; // the app which started the capture, may be unknown
@ -100,6 +104,11 @@ public class CaptureCtrl extends AppCompatActivity {
return;
}
if(action.equals(ACTION_PEER_INFO)) {
getPeerInfo();
return;
}
// Check if a control permission rule was set
mPermissions = PCAPdroid.getInstance().getCtrlPermissions();
AppDescriptor app = getCallingApp();
@ -260,4 +269,31 @@ public class CaptureCtrl extends AppCompatActivity {
intent.putExtra("pkts_rcvd", stats.pkts_rcvd);
intent.putExtra("pkts_dropped", stats.pkts_dropped);
}
// A request sent from a debug build of PCAPdroid to a non-debug one
private void getPeerInfo() {
// Verify the peer app
String package_name = getCallingPackage();
if((package_name == null) || !package_name.equals(BuildConfig.APPLICATION_ID + ".debug")) {
Log.w(TAG, "getPeerInfo: package name mismatch");
abort();
return;
}
Billing billing = Billing.newInstance(this);
billing.setLicense(billing.getLicense());
Intent res = new Intent();
HashSet<String> purchased = new HashSet<>();
for(String sku: Billing.ALL_SKUS) {
if(billing.isPurchased(sku))
purchased.add(sku);
}
res.putExtra("skus", purchased);
setResult(RESULT_OK, res);
finish();
}
}

View File

@ -38,8 +38,6 @@ import androidx.core.content.res.ResourcesCompat;
import com.emanuelef.remote_capture.Utils;
import java.util.Arrays;
import cat.ereza.customactivityoncrash.CustomActivityOnCrash;
import cat.ereza.customactivityoncrash.R;
import cat.ereza.customactivityoncrash.config.CaocConfig;
@ -158,7 +156,7 @@ public final class ErrorActivity extends AppCompatActivity {
}
private String getErrorDetails() {
return "Build type: " + Utils.getBuildType(this).toString().toLowerCase() + "\n" +
return "Build type: " + Utils.getVerifiedBuild(this).toString().toLowerCase() + "\n" +
CustomActivityOnCrash.getAllErrorDetailsFromIntent(ErrorActivity.this, getIntent());
}
}

View File

@ -27,6 +27,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.UriPermission;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
@ -61,12 +62,15 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import com.emanuelef.remote_capture.AppsResolver;
import com.emanuelef.remote_capture.Billing;
import com.emanuelef.remote_capture.BuildConfig;
import com.emanuelef.remote_capture.CaptureHelper;
import com.emanuelef.remote_capture.MitmReceiver;
import com.emanuelef.remote_capture.fragments.ConnectionsFragment;
import com.emanuelef.remote_capture.fragments.StatusFragment;
import com.emanuelef.remote_capture.interfaces.AppStateListener;
import com.emanuelef.remote_capture.model.AppDescriptor;
import com.emanuelef.remote_capture.model.AppState;
import com.emanuelef.remote_capture.CaptureService;
import com.emanuelef.remote_capture.model.CaptureSettings;
@ -82,6 +86,8 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.HashSet;
public class MainActivity extends BaseActivity implements NavigationView.OnNavigationItemSelectedListener {
private Billing mIab;
@ -121,6 +127,8 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
registerForActivityResult(new RequestPermission(), isGranted ->
Log.d(TAG, "Write permission " + (isGranted ? "granted" : "denied"))
);
private final ActivityResultLauncher<Intent> peerInfoLauncher =
registerForActivityResult(new StartActivityForResult(), this::peerInfoResult);
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -143,6 +151,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
mIab = Billing.newInstance(this);
mIab.setLicense(mIab.getLicense());
initPeerAppInfo();
initAppState();
checkPermissions();
@ -316,6 +325,65 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
}
}
// On debug builds, if the user also has the non-debug app installed (peer app), unlock the
// already-purchased features also on this beta app
private void initPeerAppInfo() {
if(!BuildConfig.DEBUG)
return;
final String peerAppPackage = "com.emanuelef.remote_capture";
AppDescriptor peer = AppsResolver.resolve(getPackageManager(), peerAppPackage, 0);
if(peer == null) {
Log.d(TAG, "Peer app not found");
return;
}
PackageInfo pInfo = peer.getPackageInfo();
if((pInfo == null) || (pInfo.versionCode < 56)) {
Log.d(TAG, "Unsupported peer app version found");
return;
}
// Verify that the peer signature
Utils.BuildType buildType = Utils.getVerifiedBuild(this, peerAppPackage);
if((buildType != Utils.BuildType.FDROID) && (buildType != Utils.BuildType.PLAYSTORE) && (buildType != Utils.BuildType.GITHUB)) {
Log.d(TAG, "Unsupported peer app build: " + buildType.name());
return;
}
Log.d(TAG, "Valid peer app found (" + pInfo.versionName + " - " + pInfo.versionCode + ")");
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setClassName(peerAppPackage, "com.emanuelef.remote_capture.activities.CaptureCtrl");
intent.putExtra("action", "get_peer_info");
try {
peerInfoLauncher.launch(intent);
} catch (ActivityNotFoundException e) {
Log.d(TAG, "Peer app launch failed");
}
}
private void peerInfoResult(final ActivityResult result) {
if((result.getResultCode() == RESULT_OK) && (result.getData() != null)) {
Intent data = result.getData();
Serializable skus_extra = data.getSerializableExtra("skus");
if(skus_extra instanceof HashSet) {
HashSet<String> skus = (HashSet<String>) skus_extra;
Log.d(TAG, "Found peer app info");
for(String sku: skus) {
Log.d(TAG, "Peer sku: " + sku);
mIab.addPeerSku(sku);
}
return;
}
}
Log.d(TAG, "Invalid peer app result");
}
private static class MainStateAdapter extends FragmentStateAdapter {
MainStateAdapter(final FragmentActivity fa) { super(fa); }