Копировать/передавать конфигурации между платными/бесплатными версиями приложения для Android?

Приложение My Android доступно как в бесплатной, так и в платной версии. Я создал проект библиотеки и два дополнительных проекта приложений, одну «бесплатную» и одну «платную» версию (конечно, подписанную одним и тем же ключом). Обратите внимание, что эти проекты приложений практически пусты, без настроек и т. д. Следовательно, библиотека содержит 99% кода.

Мое приложение создает как базу данных SQLite, так и файл SharedPreferences с пользовательскими данными. Можно ли скопировать эти файлы между бесплатной и платной версиями? (Предпочтения важнее, чем база данных.)

E.g.

  1. Пользователь запускает бесплатную версию. Создается база данных и файл конфигурации.
  2. Пользователь устанавливает платную версию и запускает ее.
  3. Платная версия проверяет данные бесплатной версии и копирует их. Это то, что я хочу!

person l33t    schedule 30.01.2012    source источник


Ответы (3)


  1. Реализуйте ContentProvider для предоставления сохраненных данных в вашей бесплатной версии.
  2. Убедитесь, что провайдер экспортирован (android:exported="true")
  3. Объявите разрешение в клиентском приложении. Уровень защиты должен быть "сигнатурный".
  4. Требовать разрешение, объявленное в (3), как readPermission для провайдера.
  5. В платном приложении добавьте разрешение на использование для разрешения, объявленного в бесплатном приложении.
  6. Проверьте наличие провайдера и загрузите данные в свое платное приложение.

Это, конечно, работает только в том случае, если вы подписываете бесплатные и платные приложения одним и тем же сертификатом (что и делают большинство здравомыслящих людей).

person Jens    schedule 30.01.2012
comment
Большое спасибо! Действительно аккуратное решение! - person l33t; 31.01.2012
comment
Разве нельзя было бы иметь одно и то же android:sharedUserId и как-то читать приложение №1 SharedPreferences из приложения №2? - person l33t; 31.01.2012
comment
Да, это тоже работает, но если вы выберете (или забудете добавить) другой sharedUserId, вы в значительной степени облажались — с разрешением вам нужно только сохранить свои ключи (которые в любом случае связаны с вашей учетной записью Market). - person Jens; 31.01.2012
comment
Если бесплатное приложение будет удалено до того, как будет установлено платное, сохранится ли ContentProvider? - person Hartok; 03.02.2013
comment
Что делать, если пользователь устанавливает только платную версию без бесплатной? Я думаю, это нормально, если вы покупаете платную версию, чтобы удалить бесплатную версию. Как тогда будет осуществляться доступ к контент-провайдеру? - person WindRider; 04.07.2013

Если вы не хотите заморачиваться с внедрением 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);
    }
}

В манифесте обязательно объявите этот класс как Получатель с именем действия, соответствующим 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. Затрудните получение ответа, используя каждый раз уникальный код действия.

Ни один из них не является доказательством дурака. Если вы просто передаете любимые цвета приложений, вам, вероятно, не нужны никакие меры безопасности. Если вы передаете более конфиденциальную информацию, вам нужно и то, и другое, и вам нужно подумать о том, чтобы сделать пароль надлежащим образом безопасным.

Другое улучшение

person Neil Townsend    schedule 31.10.2014
comment
Интересный! Спасибо! :) Гарантированно ли работает широковещательный приемник? Например. службы, как правило, убиваются операционной системой Android при нехватке ресурсов. Не уверен, что это относится и к ресиверам. - person l33t; 04.11.2014
comment
@ I33t Службы действительно убиты. Broadcastreceivers не убиваются, потому что они не всегда работают: в принципе Broadcastreceiver, зарегистрированный в манифесте, должен быть вызван (если он еще не существует), если выдано намерение, что он должен получать. - person Neil Townsend; 05.11.2014

Я собрал информацию из нескольких ответов stackoverflow, чтобы предоставить способ скопировать все данные SharedPreference из одного приложения в другое. В моем конкретном случае я использую вкусы продукта для бесплатного и профессионального приложения, и я хочу скопировать из бесплатного в профессиональное.

ВНИМАНИЕ: это работает только в том случае, если вы не выпустили ни одну из версий в магазине игр. Если вы добавите (или удалите) sharedUserId в свой приложение после того, как оно появится в магазине игр, ваши пользователи не смогут обновить его без удаления. Я научился этому на собственном горьком опыте. Спасибо Гугл..

Добавьте sharedUserId в свой манифест в обоих приложениях. Обратите внимание, что это будет работать только в том случае, если оба приложения подписаны одним и тем же сертификатом.

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

Затем вызовите этот метод при первой инициализации профессионального приложения.

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

Кроме того, в соответствии с этим ответом вы захотите вызвать getSettingsFromFreeApp() перед любым другим вызовом, чтобы получить настройки в своем приложении.

person Carson Holzheimer    schedule 10.09.2017