Copy/share configurations between paid/free versio

2020-05-21 05:56发布

问题:

My Android app comes both as a free and paid version. I have created a library project and two additional Application projects, one 'Free' and one 'Paid' version (signed with the same key, of course). Note that these Application projects are pretty much empty, no settings etc. Hence, the library contains 99% of the code.

My app creates both an SQLite database and a SharedPreferences file with user data. Is it possible to copy these files between the free and paid versions? (The preferences are more important than the database.)

E.g.

  1. User runs the free version. A database and configuration file are created.
  2. User installs the paid version and runs it.
  3. The paid version checks for any free version data and copies it. This is what I want!

回答1:

  1. Implement a ContentProvider to expose the stored data in your free version.
  2. Ensure the provider is exported (android:exported="true")
  3. Declare a permission in your client application. The protection level should be "signature".
  4. Require the permission declared in (3) as a readPermission for the provider.
  5. In your paid app, add a uses-permission for the permission declared in your free app.
  6. Check for the presence of the provider & load the data into your paid app.

This, of course, only works if you are signing the free and paid apps with the same cert (which most sane people do).



回答2:

If you don't wish to go to the trouble of implementing a ContentProvider, or if it is possible that both apps may remain installed and used, there is a different solution.

Code and usage

Let us assume that the data in question is in a class:

class DataToBeShared() {
    // Data etc in here
}

Then, add a class to both apps as follows:

public class StoredInfoManager {
    public static String codeAppType   = "apptype";
    public static String codeTimestamp = "timestamp";
    public static String codeData      = "data";
    public static String codeResponseActionString = "arstring";

    public static String responseActionString = "com.me.my.app.DATA_RESPONSE";

    private static int APP_UNKNOWN = 0;
    private static int APP_FREE    = 1;
    private static int APP_PAID    = 2;

    private static String freeSharedPrefName = "com.me.my.app.free.data";
    private static String paidSharedPrefName = "com.me.my.app.paid.data";

    // Use only one pair of the next lines depending on which app this is:
    private static String prefName = freeSharedPrefName;
    private static int    appType  = APP_FREE;

    //private static String prefName = paidSharedPrefName;
    //private static int    appType  = APP_PAID;

    private static String codeActionResponseString = "response";

    // Provide access points for the apps to store the data
    public static void storeDataToPhone(Context context, DataToBeShared data) {
        SharedPreferences settings = context.getSharedPreferences(prefName, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = settings.edit();

        // Put the data in the shared preferences using standard commends.
        // See the android developer page for SharedPreferences.Editor for details.
        // Code for that here

        // And store it
        editor.commit();
    }

So far, this is a fairly standard shared preferences storage system. Now is where the fun starts. First, make sure that there is a private method for getting the data stored above, and a private method for broadcasting it.

    private static DataToBeshared getData(Context context) {
        SharedPreferences settings = context.getSharedPreferences(prefName, Context.MODE_PRIVATE);
        DataToBeShared result = new DataToBeShared();

        // Your code here to fill out result from Shared preferences.
        // See the developer page for SharedPreferences for details.

        // And return the result.
        return result;
    }

    private static void broadcastData(Context context, DataToBeShared data, String intentActionName) {
        Bundle bundle = new Bundle();
        bundle.putInt(codeAppType, appType);
        bundle.putParcelable(codeData, data);

        Intent intent = new Intext(intentActionString);
        intent.putEXtras(bundle);
        context.sendBroadcast(intent);
    }

Create a BroadcastReceiver class to catch data responses from the other app for our data:

static class CatchData extends BroadcastReceiver {
    DataToBeShared data = null;
    Long           timestamp = 0L;
    int            versionListeningFor = Version.VERSION_UNKNOWN;
    Timeout        timeout = null;

    // We will need a timeout in case the other app isn't actually there.
    class Timeout extends CountDownTimer {
        Context _context;
        public Timeout(Context context, long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
            _context = context;
        }

        @Override
        public void onFinish() {
            broadcastAndCloseThisBRdown(_context);
        }

        @Override
        public void onTick(long millisUntilFinished) {}
    }

    // Constructor for the catching class
    // Set the timeout as you see fit, but make sure that
    // the tick length is longer than the timeout.
    CatchDPupdate(Context context, DataToBeShared dptsKnown, Long timeKnown, int otherVersion) {
        data                = dptsKnown;
        timestamp           = timeKnown;
        versionListeningFor = otherVersion;

        timeout = new Timeout(context, 5000, 1000000);
        timeout.start();
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        Bundle extras = intent.getExtras();
        if (extras == null) return;

        // Check it's the data we want
        int sendingVersion = extras.getInt(codeAppType, APP_UNKNOWN);
        if (sendingVersion != versionListeningFor) return;

        // This receiver has served its purpose, so unregister it.
        context.unregisterReceiver(this);

        // We've got the data we want, so drop the timeout.
        if (timeout != null) {
            timeout.cancel();
            timeout = null;
        }

        Long            tsInc  = extras.getLong(codeTimestamp, 0L);
        DataToBeShared  dataInc = extras.getParcelable(codeData);

        // Now, you need to decide which set of data is better.
        // You may wish to use a timestamp system incorporated in DataToBeStored.
        if (/* Incoming data best*/) {
            data        = dpInc;
            // Make it ours for the future
            storeDataToPhone(context, data);
        }

        // Send the data out
        broadcastAndCloseThisBRdown(context);
    }

    private void broadcastAndCloseThisBRdown(Context context) {
        broadcastData(context, data, responseActionString);
    }
}

Now, provide the static access function for the apps to use. Note that it doesn't return anything, that's done by the response catcher above.

    public static void geDataFromPhone(Context context) {
        DataToBeStored myData = getData(context);
        // See security discussion point 2 for this next line
        String internalResponseActionString = "com.me.my.app.blah.hohum." + UUID.randomUUID();

        // Instantiate a receiver to catch the response from the other app
        int otherAppType = (appType == APP_PAID ? APP_FREE : APP_PAID);
        CatchData catchData = new CatchData(context, mydata, otherAppType);
        context.registerReceiver(catchData, new IntentFilter(internalResponseActionString));

        // Send out a request for the data from the other app.
        Bundle bundle = new Bundle();
        bundle.putInt(codeAppType, otherAppType);
        bundle.putString(codeResponseActionString, internalResponseActionString);
        bundle.putString(CatchDataRequest.code_password, CatchDataRequest.getPassword());
        Intent intent = new Intent(responseActionString);
        context.sendBroadcast(intent);
    }

That's the core of it. We need one other class, and a tweak to the manifest. The class (to catch the requests from the other app for the data:

public class CatchDataRequest extends BroadcastReceiver {
    // See security discussion point 1 below
    public static String code_password = "com.newtsoft.android.groupmessenger.dir.p";

    public static String getPassword() {
        return calcPassword();
    }

    private static String calcPassword() {
        return "password";
    }

    private static boolean verifyPassword(String p) {
        if (p == null) return false;
        if (calcPassword().equals(p)) return true;
        return false;
    }

    @Override
    public void onReceive(Context context, Intent intent) {

        Bundle bundle = intent.getExtras();
        if (bundle == null) return;
        String passwordSent = bundle.getString(code_password);
        if (!verifyPassword(passwordSent)) return;

        int versionRequested             = bundle.getInt(StoredInfoManager.codeAppType);
        String actionStringToRespondWith = bundle.getString(StoredInfoManager.codeResponseActionString);

        // Only respond if we can offer what's asked for
        if (versionRequested != StoredInfoManager.appType) return;

        // Get the data and respond     
        DataToBrStored data = StoredInfoManager.getData(context);       
        StoredInfoManager.broadcastData(context, data, actionStringToRespondWith);
    }
}

In the manifest, be sure to declare this class as a Receiver with the action name matching StoredInfoManager.responseActionString

<receiver android:name="com.me.my.app.CatchDataRequest" android:enabled="true">
    <intent-filter>
        <action android:name="com.me.my.app.DATA_RESPONSE"/>
    </intent-filter>
</receiver>

Using this is relative simple. The class you are using the data in must extend BroadcastReceiver:

public class MyActivity extends Activity {
    // Lots of your activity code ...

    // You'll need a class to receive the data:
    MyReceiver receiver= new MyReceiver();
    class MyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Bundle extras = intent.getExtras();
            if (extras == null) return;
            // Do stuff with the data
        }
    }

    // But be sure to add the receiver lines to the following methods:
    @Override
    public void onPause() {
        super.onPause();
        this.unregisterReceiver(receiver);
    }


    @Override
    public void onResume() {
        super.onResume();
        this.registerReceiver(receiver, new IntentFilter(StoredInfoManager.receiver_action_string));
        }
    }

    // To store the data
    StoredInfoManager.storeDataToPhone(contextOfApp, data);

    // To retrieve the data is a two step process. Ask for the data:
    StoredInfoManager.getData(contextOfApp);
    // It will arrive in receiver, above.
}

Security

The weakness of this method is that anyone can register a receiver to catch the communication between the two apps. The code above circumvents this:

  1. Make the request broadcast hard to fake through the use of a password. This answer sin't a place to discuss how you might make that password secure, but it is important to realise that you can't store data when you create the password to check it against later - it's a different app that will be checking.

  2. Make the response harder to catch by using a unique action code each time.

Neither of these is fool proof. If you're simply passing around favourite app colours, you probably don't need any of the security measures. If you're passing around more sensitive information, you need both, and you need to think about making the password appropriately secure.

Other improvement

  • If you wish to check if the other version is installed before sending out the query and waiting for an answer, see Detect an application is installed or not?.


回答3:

I've collected information from a number of stackoverflow answers to provide a way to copy all SharedPreference data from one app to another. In my particular case I'm using product flavours for a free and a pro app, and I want to copy from free to pro.

CAUTION: This only works if you have not released either version on the play store. If you add (or remove) sharedUserId to your app after it is on the play store, your users won't be able to update without uninstalling. I learnt this the hard way. Thanks Google..

Add sharedUserId to your manifest in both apps. Note that this will only work if both apps are signed with the same certificate.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="my.package.name.free"
android:sharedUserId="my.package.name">

Then call this method when you first intialize the pro app.

private void getSettingsFromFreeApp() {
    // This is a build config constant to check which build flavour this is
    if (BuildConfig.IS_PRO) {
        try {
            Context otherAppContext = this.createPackageContext("my.package.name.free", Context.MODE_PRIVATE);
            SharedPreferences otherAppPrefs = PreferenceManager.getDefaultSharedPreferences(otherAppContext);

            Map<String, ?> keys = otherAppPrefs.getAll();
            SharedPreferences.Editor editor = prefs.edit();
            for(Map.Entry<String, ?> entry : keys.entrySet()){

                Object value = getWildCardType(entry.getValue());

                Log.d("map values", entry.getKey() + ": " + entry.getValue());
                if (entry.getValue() instanceof Boolean) {
                    editor.putBoolean(entry.getKey(), (boolean) value);
                    editor.apply();
                } else if (value instanceof Long) {
                    editor.putLong(entry.getKey(), (long) value);
                    editor.apply();
                } else if (value instanceof Float) {
                    editor.putFloat(entry.getKey(), (float) value);
                    editor.apply();
                } else if (value instanceof Integer) {
                    editor.putInt(entry.getKey(), (int) value);
                    editor.apply();
                } else if (value instanceof String) {
                    editor.putString(entry.getKey(), String.valueOf(value));
                    editor.apply();
                }
            }


        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }
}

private Object getWildCardType(Object value) {
   return value;
}

Also, according to this answer you will want to call getSettingsFromFreeApp() before any other call to get preferences in your app.