Android uygulamasının ilk kez kullanılıp kullanılmadığını belirleyin


112

Şu anda bir android uygulaması geliştiriyorum. Uygulama ilk kez başlatıldığında bir şeyler yapmam gerekiyor, yani kod yalnızca program ilk başlatıldığında çalışıyor.


1
Uygulamaları yapmaya ilk başladığımda, yalnızca bir uygulama yüklendikten sonraki ilk çalıştırmayı düşünüyordum. Daha sonra, yükseltmelerden sonra ilk çalıştırmaları ele almam ve farklılaştırmam gerektiğini fark ettim. @ schnatterer'in aşağıdaki cevabı ve buradaki cevabım bunun nasıl yapılacağını gösteriyor. Yükseltmeleri hesaba katmayan yanıtlara karşı dikkatli olun.
Suragch

@Suragch, yükseltmeleri hesaba katmamak kötü bir uygulama gibi davranıyorsunuz, ancak bazı durumlarda bir uygulama tanıtımı yapmak istemezsiniz :)
creativecreatorormaybenot

@creativecreatorormaybenot, bu doğru. Sadece ilk kurulumu önemsediğiniz ve sonraki yükseltmeleri önemsemediğiniz zamanlar vardır. Bu durumlar için basit bir boole yeterlidir. Ancak, ileride, son güncellemede eklediğiniz tüm yeni özellikler hakkında mevcut kullanıcılar için farklı bir giriş eklemek isterseniz ne olur? Bence boole yerine sürüm numarasını kontrol etmek daha ileri görüşlüdür. Bu, en azından size gelecekte yeni bir kurulum için bir şekilde ve bir yükseltme için başka bir şekilde yanıt verme seçeneği sunar.
Suragch

1
Öyleyse sadece bu sürüm için ekliyorsunuz ama yoz
creativecreatorormaybenot

Yanıtlar:


56

Başka bir fikir, Paylaşılan Tercihlerde bir ayar kullanmaktır. Boş bir dosyayı kontrol etmekle aynı genel fikir, ancak o zaman ortalıkta dolaşan boş bir dosyanız yok, hiçbir şey depolamak için kullanılmıyor


3
Bu tür bir yaklaşımın Android Froyo ile bir Samsung Galaxy S'de çalışamayacağına dikkat edin. Bunun nedeni, Paylaşılan Tercihler tasarrufundaki bir hatadır. İşte bununla ilgili bir SO sorusuna bağlantı: stackoverflow.com/questions/7296163/… ve işte google kodundaki bilet: code.google.com/p/android/issues/detail?id=14359
Francesco Rigoni

4
Android 6.0 (API 23 - Marshmallow) veya üstü otomatik yedeklemeye ( developer.android.com/guide/topics/data/autobackup.html ) dikkat edin, varsayılan olarak etkindir. Kullanıcılar uygulamayı kaldırır ve sonra yeniden yüklerse, paylaşılan tercihler kurtarılacaktır. Bu nedenle, yeniden yüklemelerde, herhangi bir endişe kaynağıysa, yeniden yüklemeden sonra ilk kez çalışıp çalışmadığını kontrol edemezsiniz.
Alan

1
@Alan haklısın, bu cevap artık Android Marshmallow'dan geçerli değil .
Ioane Sharvadze

1
@Alan, seninki gibi bir cevap aradığımı ne kadar zamandır tahmin edemezsin. Günümü gün ettin. Teşekkürler!
Antonio

@Alan Ancak otomatik yedekleme, diğer birçok veriyi de kaydeder. Dolayısıyla, yeniden yüklenen uygulamanın muhtemelen ilk çalıştırma durumunda olmaması beklenir. Ve kullanıcı uygulamayı daha önce zaten kullanmıştır, bu nedenle rehberliğe gerek yoktur. Bu yüzden çoğu durumda bunun iyi bir şey olduğunu iddia ediyorum.
smdufb

112

Sen kullanabilirsiniz SharedPreferences o "İlk kez" uygulaması başlatılır olup olmadığını belirlemek için. Sadece bir Boolean değişkeni ("my_first_time") kullanın ve "ilk kez" göreviniz bittiğinde değerini false olarak değiştirin .

Bu, uygulamayı ilk açtığınızda yakalayacağım koddur:

final String PREFS_NAME = "MyPrefsFile";

SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);

if (settings.getBoolean("my_first_time", true)) {
    //the app is being launched for first time, do something        
    Log.d("Comments", "First time");

             // first time task

    // record the fact that the app has been started at least once
    settings.edit().putBoolean("my_first_time", false).commit(); 
}

17
Uygulamanın Google Play Store'daki bir sonraki sürüme ne zaman güncelleneceğini ele alacak mı?
Shajeel Afzal

4
Yükseltme sırasında Paylaşılan Tercihler korunur. Bu yüzden, PlayStore'dan yükseltildiğinde eski değerin mevcut olduğunu varsayıyorum. Aslında diğer yöntemler için de geçerlidir, yani dosyanın varlığını kontrol etmek de mümkündür. Dolayısıyla, bu durumda kısayol yöntemi farklı tercih / dosya adı veya değeri kullanmaktır.
Tejasvi Hegde

@ShajeelAfzal bunun gibi bazı şeyler herkese açık void yapmanıza yardımcı olabilir CheckAndInitAppFirstTime () {final String PREFS_NAME = "TheAppVer"; final String CHECK_VERSION = "1"; // Gerekli ver ... final String KEY_NAME = "CheckVersion"; SharedPreferences ayarları = getSharedPreferences (PREFS_NAME, 0); (! settings.getString (KEY_NAME, "0"). equals (CHECK_VERSION)) {// uygulama ilk kez başlatılıyorsa, bir şeyler yapın veya CHECK_VERSION farklı // ... settings.edit (). putString ( KEY_NAME, CHECK_VERSION) .commit (); }}
Tejasvi Hegde

@aman verma: developer.android.com/reference/android/content/… adresindeki getBoolean açıklamasına göre getBoolean'ın 2. parametresi, ilk parametre çıkmazsa varsayılan değerdir, bu nedenle "my_first_time" ayarlanmadıysa ifade varsayılan olarak true olur.
user2798692

63

Sadece bir boole bayrağını değil, tam sürüm kodunu da saklamanızı öneririm. Bu şekilde, yeni bir sürümün ilk başlangıcı olup olmadığını başlangıçta da sorgulayabilirsiniz. Bu bilgiyi örneğin "Yenilikler" diyaloğunu görüntülemek için kullanabilirsiniz.

Aşağıdaki kod, "bağlam olan" herhangi bir android sınıfından (etkinlikler, hizmetler, ...) çalışmalıdır. Ayrı bir (POJO) sınıfta olmasını tercih ederseniz, örneğin burada açıklandığı gibi bir "statik bağlam" kullanmayı düşünebilirsiniz .

/**
 * Distinguishes different kinds of app starts: <li>
 * <ul>
 * First start ever ({@link #FIRST_TIME})
 * </ul>
 * <ul>
 * First start in this version ({@link #FIRST_TIME_VERSION})
 * </ul>
 * <ul>
 * Normal app start ({@link #NORMAL})
 * </ul>
 * 
 * @author schnatterer
 * 
 */
public enum AppStart {
    FIRST_TIME, FIRST_TIME_VERSION, NORMAL;
}

/**
 * The app version code (not the version name!) that was used on the last
 * start of the app.
 */
private static final String LAST_APP_VERSION = "last_app_version";

/**
 * Finds out started for the first time (ever or in the current version).<br/>
 * <br/>
 * Note: This method is <b>not idempotent</b> only the first call will
 * determine the proper result. Any subsequent calls will only return
 * {@link AppStart#NORMAL} until the app is started again. So you might want
 * to consider caching the result!
 * 
 * @return the type of app start
 */
public AppStart checkAppStart() {
    PackageInfo pInfo;
    SharedPreferences sharedPreferences = PreferenceManager
            .getDefaultSharedPreferences(this);
    AppStart appStart = AppStart.NORMAL;
    try {
        pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
        int lastVersionCode = sharedPreferences
                .getInt(LAST_APP_VERSION, -1);
        int currentVersionCode = pInfo.versionCode;
        appStart = checkAppStart(currentVersionCode, lastVersionCode);
        // Update version in preferences
        sharedPreferences.edit()
                .putInt(LAST_APP_VERSION, currentVersionCode).commit();
    } catch (NameNotFoundException e) {
        Log.w(Constants.LOG,
                "Unable to determine current app version from pacakge manager. Defenisvely assuming normal app start.");
    }
    return appStart;
}

public AppStart checkAppStart(int currentVersionCode, int lastVersionCode) {
    if (lastVersionCode == -1) {
        return AppStart.FIRST_TIME;
    } else if (lastVersionCode < currentVersionCode) {
        return AppStart.FIRST_TIME_VERSION;
    } else if (lastVersionCode > currentVersionCode) {
        Log.w(Constants.LOG, "Current version code (" + currentVersionCode
                + ") is less then the one recognized on last startup ("
                + lastVersionCode
                + "). Defenisvely assuming normal app start.");
        return AppStart.NORMAL;
    } else {
        return AppStart.NORMAL;
    }
}

Bunun gibi bir etkinlikten kullanılabilir:

public class MainActivity extends Activity {        
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        switch (checkAppStart()) {
        case NORMAL:
            // We don't want to get on the user's nerves
            break;
        case FIRST_TIME_VERSION:
            // TODO show what's new
            break;
        case FIRST_TIME:
            // TODO show a tutorial
            break;
        default:
            break;
        }

        // ...
    }
    // ...
}

Temel mantık, bu JUnit testi kullanılarak doğrulanabilir:

public void testCheckAppStart() {
    // First start
    int oldVersion = -1;
    int newVersion = 1;
    assertEquals("Unexpected result", AppStart.FIRST_TIME,
            service.checkAppStart(newVersion, oldVersion));

    // First start this version
    oldVersion = 1;
    newVersion = 2;
    assertEquals("Unexpected result", AppStart.FIRST_TIME_VERSION,
            service.checkAppStart(newVersion, oldVersion));

    // Normal start
    oldVersion = 2;
    newVersion = 2;
    assertEquals("Unexpected result", AppStart.NORMAL,
            service.checkAppStart(newVersion, oldVersion));
}

Biraz daha çaba sarf ederek muhtemelen android ile ilgili şeyleri (PackageManager ve SharedPreferences) da test edebilirsiniz. Testi yazmakla ilgilenen var mı? :)

Yukarıdaki kodun yalnızca android:versionCodeAndroidManifest.xml'deki ile uğraşmazsanız düzgün çalışacağını unutmayın !


2
Lütfen bu yöntemin nasıl kullanılacağını açıklayın. SharedPreferences nesnesini nerede başlatıyorsunuz?
Shajeel Afzal

1
benim için çalışmıyor - her zaman ilk
eğitimimi başlatıyor

1
Bu kod çok daha yalındır bağlam ve başka yerlerde tercihlerini ilan yan etkileri olmadan public AppStart checkAppStart(Context context, SharedPreferences sharedPreferences)çok daha iyi bir yöntem imzadır
Will

2
Bu cevabın bir güncelleme özünü burada yaptım gist.github.com/williscool/2a57bcd47a206e980eee Orjinal kodla ilgili bir sorun yaşadım ve bunun sonsuza kadar izlenecek döngüde takılı kalmasına neden oldu çünkü sürüm numarası ilk checkAppStartblokta asla yeniden hesaplanmıyordu . bu yüzden benim güncellenen kod paylaşmak ve herkesin bu konuda öneri olup olmadığını görmek için karar
Will

1
@ Girişiniz için teşekkürler. Haklısın, kod basitleştirilebilir ve daha sağlam hale getirilebilir. Cevabı ilk gönderdiğimde, kodu farklı etkinliklerden erişmek istediğim daha karmaşık bir senaryodan çıkardım AppStart. Bu yüzden mantığı ayrı bir hizmet yöntemine koydum. Bu nedenle bir contextdeğişken vardı ve AppStartidempotent yöntem çağrılarını kolaylaştırmak için statik bir değişken içinde saklandı.
schnatterer

4

Güncelleme olup olmadığına bağlı olarak uygulamanın ilk seferiniz olup olmadığını belirlemek için çözdüm.

private int appGetFirstTimeRun() {
    //Check if App Start First Time
    SharedPreferences appPreferences = getSharedPreferences("MyAPP", 0);
    int appCurrentBuildVersion = BuildConfig.VERSION_CODE;
    int appLastBuildVersion = appPreferences.getInt("app_first_time", 0);

    //Log.d("appPreferences", "app_first_time = " + appLastBuildVersion);

    if (appLastBuildVersion == appCurrentBuildVersion ) {
        return 1; //ya has iniciado la appp alguna vez

    } else {
        appPreferences.edit().putInt("app_first_time",
                appCurrentBuildVersion).apply();
        if (appLastBuildVersion == 0) {
            return 0; //es la primera vez
        } else {
            return 2; //es una versión nueva
        }
    }
}

Hesaplama sonuçları:

  • 0: Bu ilk defa ise.
  • 1: Hiç başladı.
  • 2: Bir kez başladı, ancak o sürüm değil, yani bir güncelleme.

3

Android SharedPreferences'ı kullanabilirsiniz .

Android SharedPreferences, özel ilkel uygulama verilerini anahtar / değer çifti biçiminde depolamamıza olanak tanır.

KOD

Özel bir sınıf SharedPreference oluşturun

 public class SharedPreference {

    android.content.SharedPreferences pref;
    android.content.SharedPreferences.Editor editor;
    Context _context;
    private static final String PREF_NAME = "testing";

    // All Shared Preferences Keys Declare as #public
    public static final String KEY_SET_APP_RUN_FIRST_TIME       =        "KEY_SET_APP_RUN_FIRST_TIME";


    public SharedPreference(Context context) // Constructor
    {
        this._context = context;
        pref = _context.getSharedPreferences(PREF_NAME, 0);
        editor = pref.edit();

    }

    /*
    *  Set Method Generally Store Data;
    *  Get Method Generally Retrieve Data ;
    * */


    public void setApp_runFirst(String App_runFirst)
    {
        editor.remove(KEY_SET_APP_RUN_FIRST_TIME);
        editor.putString(KEY_SET_APP_RUN_FIRST_TIME, App_runFirst);
        editor.apply();
    }

    public String getApp_runFirst()
    {
        String  App_runFirst= pref.getString(KEY_SET_APP_RUN_FIRST_TIME, "FIRST");
        return  App_runFirst;
    }

}

Şimdi Aktivitenizi Açın ve Başlatın .

 private     SharedPreference                sharedPreferenceObj; // Declare Global

Şimdi bunu OnCreate bölümünde arayın

 sharedPreferenceObj=new SharedPreference(YourActivity.this);

Şimdi Kontrol Ediliyor

if(sharedPreferenceObj.getApp_runFirst().equals("FIRST"))
 {
   // That's mean First Time Launch
   // After your Work , SET Status NO
   sharedPreferenceObj.setApp_runFirst("NO");
 }
else
 { 
   // App is not First Time Launch
 }

2

İşte bunun için bazı kodlar -

String path = Environment.getExternalStorageDirectory().getAbsolutePath() +
                    "/Android/data/myapp/files/myfile.txt";

boolean exists = (new File(path)).exists(); 

if (!exists) {
    doSomething();                                      
}
else {
    doSomethingElse();
}

1

Boş bir dosyanın varlığını kontrol edebilirsiniz, eğer mevcut değilse, kodunuzu çalıştırabilir ve dosyayı oluşturabilirsiniz.

Örneğin

if(File.Exists("emptyfile"){
    //Your code here
    File.Create("emptyfile");
}

Bunu yapmayı düşünüyordum ama daha iyi bir yol olması gerektiğini düşündüm
Boardy

Hiç bilmiyorum, ama bununla elde ettiğiniz kaynak eksikliği nedir? Dosya için 4 bayt ve başında bir "eğer". Sistem rutinleri de aynısını yapardı, tamamen aynı şeyi yaparlar ya da önceden lanse edilmiş uygulamalarla bir tablo oluştururlardı
MechMK1

Benzer bir şekilde paylaşılan tercihleri ​​kullanabilirsiniz, eğer mevcut değilse, bir açılış ekranı vb. Gösterirsiniz ve sadece program ilk çalıştırıldığında (obv kontrol ettikten sonra) yaratırsınız. Kevin'ın yukarıdaki cevabına bakın
stealthcopter

1

Kodunuzun ilk kez / n kez çalışıp çalışmadığını kontrol etmek için basit bir sınıf yaptım!

Misal

Benzersiz tercihler oluşturun

FirstTimePreference prefFirstTime = new FirstTimePreference(getApplicationContext());

RunTheFirstTime kullanın, etkinliğinizi kontrol etmek için bir anahtar seçin

if (prefFirstTime.runTheFirstTime("myKey")) {
    Toast.makeText(this, "Test myKey & coutdown: " + prefFirstTime.getCountDown("myKey"),
                   Toast.LENGTH_LONG).show();
}

RunTheFirstNTimes kullanın, bir anahtar seçin ve kaç defa çalıştırılacağını seçin

if(prefFirstTime.runTheFirstNTimes("anotherKey" , 5)) {
    Toast.makeText(this, "ciccia Test coutdown: "+ prefFirstTime.getCountDown("anotherKey"),
                   Toast.LENGTH_LONG).show();
}
  • Kodunuzu daha iyi işlemek için getCountDown () kullanın

FirstTimePreference.java


1

Destek kitaplığı revizyonu 23.3.0'da sadece bunun için destek var (v4'te Android 1.6'ya uyumluluk anlamına geliyor).

Başlatıcı etkinliğinizde önce şu aramayı yapın:

AppLaunchChecker.onActivityCreate(activity);

Sonra ara:

AppLaunchChecker.hasStartedFromLauncher(activity);

Bu, uygulamanın ilk başlatılışıysa geri dönecektir.


AppLaunchChecker.onActivityCreate () çağrıldıktan sonra, bu çağrıların sırası tersine çevrilmelidir, AppLaunchChecker.hasStartedFromLauncher () true olarak dönecektir.
Gary Kipnis

Bu oldukça yanıltıcıdır. Uygulamanın "hiç başlatılmış" olup olmadığını söylemiyor; bunun yerine, uygulamanın "başlatıcıdan bir kullanıcı tarafından başlatılıp başlatılmadığını" belirtir. Dolayısıyla, diğer uygulamaların veya derin bağlantıların uygulamayı zaten başlatmış olma olasılığı vardır.
Farid

1

Basit bir yol arıyorsanız, işte burada.

Bunun gibi bir yardımcı sınıf oluşturun,

public class ApplicationUtils {

  /**
  * Sets the boolean preference value
  *
  * @param context the current context
  * @param key     the preference key
  * @param value   the value to be set
  */
 public static void setBooleanPreferenceValue(Context context, String key, boolean value) {
     SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
     sp.edit().putBoolean(key, value).apply();
 }

 /**
  * Get the boolean preference value from the SharedPreference
  *
  * @param context the current context
  * @param key     the preference key
  * @return the the preference value
  */
 public static boolean getBooleanPreferenceValue(Context context, String key) {
     SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
     return sp.getBoolean(key, false);
 }

}

Ana Etkinliğinizde, onCreate ()

if(!ApplicationUtils.getBooleanPreferenceValue(this,"isFirstTimeExecution")){
Log.d(TAG, "First time Execution");
ApplicationUtils.setBooleanPreferenceValue(this,"isFirstTimeExecution",true);
// do your first time execution stuff here,
}

1

kotlin için

    fun checkFirstRun() {

    var prefs_name = "MyPrefsFile"
    var pref_version_code_key = "version_code"
    var doesnt_exist: Int = -1;

    // Get current version code
    var currentVersionCode = BuildConfig.VERSION_CODE

    // Get saved version code
    var prefs: SharedPreferences = getSharedPreferences(prefs_name, MODE_PRIVATE)
    var savedVersionCode: Int = prefs.getInt(pref_version_code_key, doesnt_exist)

    // Check for first run or upgrade
    if (currentVersionCode == savedVersionCode) {

        // This is just a normal run
        return;

    } else if (savedVersionCode == doesnt_exist) {

        // TODO This is a new install (or the user cleared the shared preferences)


    } else if (currentVersionCode > savedVersionCode) {

        // TODO This is an upgrade
    }

    // Update the shared preferences with the current version code
    prefs.edit().putInt(pref_version_code_key, currentVersionCode).apply();

}

Kotlin'de cevap verdiğiniz için çok teşekkür ederim
MMG

0

Neden Veritabanı Yardımcısını kullanmıyorsunuz? Bu, yalnızca uygulama ilk başlatıldığında çağrılan güzel bir onCreate'e sahip olacaktır. Bu, takip etmeden ilk uygulama yüklendikten sonra bunu izlemek isteyen kişilere yardımcı olacaktır.


Bu bir veritabanı oluşturur mu? Gerçek bir veritabanı oluşturmadan DatabaseHelper nasıl kullanılır? Ve onCreate()her yeni sürüm için çağrıldığını düşünüyorum . Ayrıca, gereksiz olduğu veya istenmeyen bir amaç için bir şey kullanıldığı düşünülmez mi?
ADTC

onCreate yalnızca uygulama ilk kez yüklendiğinde tetiklenir. Db sürümü artırıldığında, onUpdated tetiklenir.
slott

Peki gereksiz, çok sert bir kelime :) - Eğer seçeneğiniz varsa yani. uygulamanız henüz yayında değil, ardından bir SharedPrefs bayrağı ayarlayın ve bunu ilk önyükleme olup olmadığını belirlemek için kullanın. Uygulamanın bir süredir vahşi olduğu bir vakam vardı ve bir DB kullanıyorduk, bu nedenle onCreate benim için mükemmel bir eşleşme oldu.
slott

0

Paylaşılan tercihlerimde bir "güncelleme sayısı" olmasını seviyorum. Orada değilse (veya varsayılan sıfır değeri), o zaman bu benim uygulamamın "ilk kullanımı" dır.

private static final int UPDATE_COUNT = 1;    // Increment this on major change
...
if (sp.getInt("updateCount", 0) == 0) {
    // first use
} else if (sp.getInt("updateCount", 0) < UPDATE_COUNT) {
    // Pop up dialog telling user about new features
}
...
sp.edit().putInt("updateCount", UPDATE_COUNT);

Şimdi, uygulamada kullanıcıların bilmesi gereken bir güncelleme olduğunda, UPDATE_COUNT artırıyorum


-1
    /**
     * @author ALGO
     */
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.util.UUID;

    import android.content.Context;

    public class Util {
        // ===========================================================
        //
        // ===========================================================

        private static final String INSTALLATION = "INSTALLATION";

        public synchronized static boolean isFirstLaunch(Context context) {
            String sID = null;
            boolean launchFlag = false;
            if (sID == null) {
                File installation = new File(context.getFilesDir(), INSTALLATION);
                try {
                    if (!installation.exists()) {

                        writeInstallationFile(installation);
                    }
                    sID = readInstallationFile(installation);
launchFlag = true;
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
            return launchFlag;
        }

        private static String readInstallationFile(File installation) throws IOException {
            RandomAccessFile f = new RandomAccessFile(installation, "r");// read only mode
            byte[] bytes = new byte[(int) f.length()];
            f.readFully(bytes);
            f.close();

            return new String(bytes);
        }

        private static void writeInstallationFile(File installation) throws IOException {
            FileOutputStream out = new FileOutputStream(installation);
            String id = UUID.randomUUID().toString();
            out.write(id.getBytes());
            out.close();
        }
    }

> Usage (in class extending android.app.Activity)

Util.isFirstLaunch(this);

-2

Merhaba arkadaşlar ben böyle bir şey yapıyorum. Ve benim için çalışıyor

ilk kez false olarak ayarlandıktan sonra, paylaşılan tercih içinde bir Boole alanı oluşturun. Varsayılan değer true {isFirstTime: true}. Android sistemde hiçbir şey bundan daha basit ve aktarılabilir olamaz.


Uh, yolu böyle kodlamayın! Basitçe yaparsanız Context.getSharedPreferences(), her yerde işe yaraması dışında aynı yerde son bulacaktır
Takhion

sana katılıyorum :)
DropAndTrap
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.