【问题标题】:Copy/share configurations between paid/free versions of Android app?在 Android 应用的付费/免费版本之间复制/共享配置?
【发布时间】:2012-02-22 15:35:22
【问题描述】:

我的 Android 应用提供免费和付费版本。我创建了一个库项目和两个附加的应用程序项目,一个“免费”版本和一个“付费”版本(当然,使用相同的密钥签名)。请注意,这些应用程序项目几乎是空的,没有设置等。因此,该库包含 99% 的代码。

我的应用程序同时创建了一个 SQLite 数据库和一个包含用户数据的 SharedPreferences 文件。是否可以在免费版和付费版之间复制这些文件? (偏好比数据库更重要。)

例如

  1. 用户运行免费版本。已创建数据库和配置文件。
  2. 用户安装付费版本并运行。
  3. 付费版本检查任何免费版本数据并复制它。 这就是我想要的!

【问题讨论】:

    标签: android sharedpreferences share


    【解决方案1】:
    1. 实施 ContentProvider 以在您的免费版本中公开存储的数据。
    2. 确保提供程序已导出 (android:exported="true")
    3. 在您的客户端应用程序中声明权限。保护级别应为“签名”。
    4. 需要将 (3) 中声明的权限作为提供程序的 readPermission。
    5. 在您的付费应用中,为您的免费应用中声明的权限添加使用权限。
    6. 检查提供者的存在并将数据加载到您的付费应用中。

    当然,这仅在您使用相同证书签署免费和付费应用程序时才有效(大多数理智的人都会这样做)。

    【讨论】:

    • 非常感谢!非常巧妙的解决方案!
    • 难道不能拥有相同的android:sharedUserId 并以某种方式从 App #2 中读取 App #1 SharedPreferences 吗?
    • 是的,这也行得通——但如果你选择(或忘记添加)不同的 sharedUserId,你就完蛋了——你只需要保留你的密钥(与你的密钥相关联)市场帐户)。
    • 如果在安装付费应用之前卸载了免费应用,ContentProvider还能存活吗?
    • 如果用户只安装付费版本而不安装免费版本怎么办?我认为如果您购买付费版本来卸载免费版本是正常的。届时将如何访问内容提供者?
    【解决方案2】:

    如果您不想麻烦地实施ContentProvider,或者如果两个应用程序都可能保持安装和使用状态,则有不同的解决方案。

    代码和用法

    让我们假设有问题的数据在一个类中:

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

    然后,为两个应用添加一个类,如下所示:

    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();
        }
    

    到目前为止,这是一个相当标准的共享偏好存储系统。现在是乐趣开始的地方。首先,确保有一个私有方法来获取上面存储的数据,以及一个私有方法来广播它。

        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);
        }
    

    创建一个BroadcastReceiver 类来捕获来自其他应用的数据响应以获取我们的数据:

    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);
        }
    }
    

    现在,为应用程序提供静态访问功能。请注意,它不会返回任何内容,这是由上面的响应捕获器完成的。

        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);
        }
    

    这是它的核心。我们需要另一个类,并对清单进行调整。类(用于捕获来自其他应用的数据请求:

    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);
        }
    }
    

    在清单中,请务必将此类声明为 Receiver,其操作名称与 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>
    

    使用它相对简单。您使用数据的类必须扩展 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.
    }
    

    安全

    这种方法的弱点是任何人都可以注册一个接收器来捕获两个应用程序之间的通信。上面的代码绕过了这个:

    1. 通过使用密码使请求广播难以伪造。这个答案不是讨论如何使密码安全的地方,但重要的是要意识到当您创建密码以稍后检查时您无法存储数据 - 这是一个不同的应用程序将进行检查。

    2. 每次都使用唯一的操作代码,使响应更难捕捉。

    这些都不是万无一失的。如果您只是传递最喜欢的应用颜色,您可能不需要任何安全措施。如果您要传递更敏感的信息,则两者都需要,并且您需要考虑使密码适当安全。

    其他改进

    【讨论】:

    • 有趣!谢谢! :) 广播接收器是否保证正在运行?例如。当资源耗尽时,服务往往会被 Android 操作系统杀死。不确定这是否也适用于接收者。
    • @I33t 服务确实被杀死了。 Broadcastreceiver 不会被杀死,因为它们并不总是在运行:原则上,如果发出了应该接收的意图,则应该调用在 manifest 中注册的 Broadcastreceiver(如果它不存在)。跨度>
    【解决方案3】:

    我从许多 stackoverflow 答案中收集了信息,以提供一种将所有 SharedPreference 数据从一个应用程序复制到另一个应用程序的方法。在我的特殊情况下,我将产品风格用于免费和专业应用程序,我想从免费复制到专业。

    注意:这仅适用于您尚未在 Play 商店中发布任一版本的情况。如果您在 Play 商店中添加(或删除)sharedUserId 到您的应用程序,您的用户将无法在不卸载的情况下进行更新。我很难学到这一点。谢谢谷歌..

    将 sharedUserId 添加到两个应用程序的清单中。请注意,这只有在两个应用都使用相同的证书签名时才有效。

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

    然后在你第一次初始化 pro 应用时调用这个方法。

    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;
    }
    

    此外,根据this answer,您需要先拨打getSettingsFromFreeApp(),然后再拨打电话以获取您应用中的首选项。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-06-19
      • 1970-01-01
      • 1970-01-01
      • 2015-09-09
      • 1970-01-01
      • 2018-01-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多