Android Room-简单选择查询-无法访问主线程上的数据库


124

我正在使用Room Persistence Library尝试一个示例。我创建了一个实体:

@Entity
public class Agent {
    @PrimaryKey
    public String guid;
    public String name;
    public String email;
    public String password;
    public String phone;
    public String licence;
}

创建了一个DAO类:

@Dao
public interface AgentDao {
    @Query("SELECT COUNT(*) FROM Agent where email = :email OR phone = :phone OR licence = :licence")
    int agentsCount(String email, String phone, String licence);

    @Insert
    void insertAgent(Agent agent);
}

创建数据库类:

@Database(entities = {Agent.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract AgentDao agentDao();
}

在Kotlin中使用以下子类公开的数据库:

class MyApp : Application() {

    companion object DatabaseSetup {
        var database: AppDatabase? = null
    }

    override fun onCreate() {
        super.onCreate()
        MyApp.database =  Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").build()
    }
}

在我的活动中实现了以下功能:

void signUpAction(View view) {
        String email = editTextEmail.getText().toString();
        String phone = editTextPhone.getText().toString();
        String license = editTextLicence.getText().toString();

        AgentDao agentDao = MyApp.DatabaseSetup.getDatabase().agentDao();
        //1: Check if agent already exists
        int agentsCount = agentDao.agentsCount(email, phone, license);
        if (agentsCount > 0) {
            //2: If it already exists then prompt user
            Toast.makeText(this, "Agent already exists!", Toast.LENGTH_LONG).show();
        }
        else {
            Toast.makeText(this, "Agent does not exist! Hurray :)", Toast.LENGTH_LONG).show();
            onBackPressed();
        }
    }

不幸的是,在执行上述方法时,它在以下堆栈跟踪中崩溃:

    FATAL EXCEPTION: main
 Process: com.example.me.MyApp, PID: 31592
java.lang.IllegalStateException: Could not execute method for android:onClick
    at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:293)
    at android.view.View.performClick(View.java:5612)
    at android.view.View$PerformClick.run(View.java:22288)
    at android.os.Handler.handleCallback(Handler.java:751)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:154)
    at android.app.ActivityThread.main(ActivityThread.java:6123)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757)
 Caused by: java.lang.reflect.InvocationTargetException
    at java.lang.reflect.Method.invoke(Native Method)
    at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288)
    at android.view.View.performClick(View.java:5612) 
    at android.view.View$PerformClick.run(View.java:22288) 
    at android.os.Handler.handleCallback(Handler.java:751) 
    at android.os.Handler.dispatchMessage(Handler.java:95) 
    at android.os.Looper.loop(Looper.java:154) 
    at android.app.ActivityThread.main(ActivityThread.java:6123) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757) 
 Caused by: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long periods of time.
    at android.arch.persistence.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:137)
    at android.arch.persistence.room.RoomDatabase.query(RoomDatabase.java:165)
    at com.example.me.MyApp.RoomDb.Dao.AgentDao_Impl.agentsCount(AgentDao_Impl.java:94)
    at com.example.me.MyApp.View.SignUpActivity.signUpAction(SignUpActivity.java:58)
    at java.lang.reflect.Method.invoke(Native Method) 
    at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288) 
    at android.view.View.performClick(View.java:5612) 
    at android.view.View$PerformClick.run(View.java:22288) 
    at android.os.Handler.handleCallback(Handler.java:751) 
    at android.os.Handler.dispatchMessage(Handler.java:95) 
    at android.os.Looper.loop(Looper.java:154) 
    at android.app.ActivityThread.main(ActivityThread.java:6123) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757) 

似乎该问题与在主线程上执行db操作有关。但是,以上链接中提供的示例测试代码未在单独的线程上运行:

@Test
    public void writeUserAndReadInList() throws Exception {
        User user = TestUtil.createUser(3);
        user.setName("george");
        mUserDao.insert(user);
        List<User> byName = mUserDao.findUsersByName("george");
        assertThat(byName.get(0), equalTo(user));
    }

我在这里想念什么吗?如何使它执行而不会崩溃?请提出建议。


1
尽管是为Kotlin撰写的,但这篇文章很好地解释了潜在的问题!
Peter Lehnhardt


看这个答案。这个答案对我
有用

Answers:


60

像Dale说的那样,锁定UI的主线程上的数据库访问是错误。

在Activity扩展AsyncTask中创建一个静态嵌套类(以防止内存泄漏)。

private static class AgentAsyncTask extends AsyncTask<Void, Void, Integer> {

    //Prevent leak
    private WeakReference<Activity> weakActivity;
    private String email;
    private String phone;
    private String license;

    public AgentAsyncTask(Activity activity, String email, String phone, String license) {
        weakActivity = new WeakReference<>(activity);
        this.email = email;
        this.phone = phone;
        this.license = license;
    }

    @Override
    protected Integer doInBackground(Void... params) {
        AgentDao agentDao = MyApp.DatabaseSetup.getDatabase().agentDao();
        return agentDao.agentsCount(email, phone, license);
    }

    @Override
    protected void onPostExecute(Integer agentsCount) {
        Activity activity = weakActivity.get();
        if(activity == null) {
            return;
        }

        if (agentsCount > 0) {
            //2: If it already exists then prompt user
            Toast.makeText(activity, "Agent already exists!", Toast.LENGTH_LONG).show();
        } else {
            Toast.makeText(activity, "Agent does not exist! Hurray :)", Toast.LENGTH_LONG).show();
            activity.onBackPressed();
        }
    }
}

或者,您可以在自己的文件上创建一个最终类。

然后在signUpAction(View view)方法中执行它:

new AgentAsyncTask(this, email, phone, license).execute();

在某些情况下,您可能还希望在活动中保留对AgentAsyncTask的引用,以便在活动被销毁时可以将其取消。但是您必须自己中断任何交易。

另外,您对Google测试示例的问题...在该网页中指出:

建议的测试数据库实现的方法是编写可在Android设备上运行的JUnit测试。因为这些测试不需要创建活动,所以它们的执行速度应该比UI测试更快。

没有活动,没有用户界面。

- 编辑 -

对于想知道的人...您还有其他选择。我建议看一下新的ViewModel和LiveData组件。LiveData与Room搭配使用效果很好。 https://developer.android.com/topic/libraries/architecture/livedata.html

另一个选择是RxJava / RxAndroid。比LiveData更强大,但更复杂。 https://github.com/ReactiveX/RxJava

-编辑2--

由于可能会有很多人遇到这个答案...一般来说,当今最好的选择是Kotlin Coroutines。Room现在直接支持它(当前为beta)。 https://kotlinlang.org/docs/reference/coroutines-overview.html https://developer.android.com/jetpack/androidx/releases/room#2.1.0-beta01


29
每当我需要访问数据库时,我是否应该执行这个巨大的异步任务(您在代码示例中已提出)?这是一打左右的代码行,而不是从db获取一些数据的代码行。您还建议创建一个新类,但这是否意味着我需要为每个插入/选择数据库调用创建一个新的AsyncTask类?
Piotrek '17

7
您还有其他选择,是的。您可能想看看新的ViewModel和LiveData组件。使用LiveData时,您不需要AsyncTask,只要发生任何更改,该对象都会收到通知。developer.android.com/topic/libraries/architecture/... developer.android.com/topic/libraries/architecture/... 还有AndroidRx(虽然它几乎LiveData做什么),并承诺。使用AsyncTask时,可以以一种方式进行体系结构,即可以在一个AsyncTask中包含多个操作,也可以将每个操作分开。
mcastro

@Piotrek-Kotlin现在已经异步烘焙(尽管已标记为实验性)。看到我的回答比较琐碎。塞缪尔·罗伯特(Samuel Robert)的答案涉及Rx。我在这里看不到LiveData的答案,但是如果您要观察的话,那可能是更好的选择。
AjahnCharles

使用常规的AsyncTask甚至现在似乎都无法正常工作,但仍然会收到非法的状态异常
Peterstev Uremgba '18

@Piotrek,您是告诉我您习惯于在整个体验中执行主线程上的数据库访问吗?
mr5

142

不建议这样做,但是您可以使用以下命令访问主线程上的数据库 allowMainThreadQueries()

MyApp.database =  Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").allowMainThreadQueries().build()

9
除非您调用allowMainThreadQueries()了构建器,否则Room不允许访问主线程上的数据库,因为它可能会长时间锁定UI。异步查询(返回查询LiveData或RxJava的查询Flowable)不受此规则的限制,因为它们在需要时在后台线程上异步运行查询。
2013年

4
谢谢,这对于迁移非常有用,因为我想测试Room是否按预期工作,然后再从
加载程序

5
@JideGuruTheProgrammer不,不应该。在某些情况下,这可能会大大降低您的应用程序运行速度。该操作应异步进行。
亚历克斯

@Alex永远不会在主线程上进行查询?
贾斯汀·迈纳斯

1
@JustinMeiners这只是一个不好的做法,只要数据库很小,您就可以这样做。
lasec0203

51

Kotlin协程(清晰简洁)

AsyncTask确实很笨拙。协程是一种更干净的替代方法(只需添加几个关键字,同步代码就会变为异步)。

// Step 1: add `suspend` to your fun
suspend fun roomFun(...): Int
suspend fun notRoomFun(...) = withContext(Dispatchers.IO) { ... }

// Step 2: launch from coroutine scope
private fun myFun() {
    lifecycleScope.launch { // coroutine on Main
        val queryResult = roomFun(...) // coroutine on IO
        doStuff() // ...back on Main
    }
}

依赖关系(为拱门组件添加协程作用域):

// lifecycleScope:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha04'

// viewModelScope:
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha04'

-更新:
2019年5月8日: Room 2.1现在支持suspend
2019年9月13日:更新为使用架构组件范围


1
@Query abstract suspend fun count()通过使用suspend关键字,您是否有任何编译错误?您能好好研究一下这个类似的问题吗:stackoverflow.com/questions/48694449/…–
罗宾

@Robin-是的,我愿意。我的错; 我在一个称为受保护的非暂挂函数的公共(未注释)DAO方法上使用了暂挂@Query。当我也将suspend关键字添加到内部@Query方法时,确实确实无法编译。看起来很聪明,可以进行挂起和Room冲突(就像您在另一个问题中提到的那样,suspend的编译版本正在返回Room无法处理的延续)。
AjahnCharles

很有道理。我将用协程函数来代替它。
罗宾

1
@Robin-仅供参考,他们增加了对在2.1会议室中暂停的支持:)
AjahnCharles

显然没有launch关键字了,您可以使用范围启动,例如GlobalScope.launch
nasch

48

对于在那里的所有RxJavaRxAndroidRxKotlin爱好者

Observable.just(db)
          .subscribeOn(Schedulers.io())
          .subscribe { db -> // database operation }

3
如果我将此代码放在方法中,如何从数据库操作返回结果?
Eggakin Baconwalker

@EggakinBaconwalker我有我要去的override fun getTopScores(): Observable<List<PlayerScore>> { return Observable .fromCallable({ GameApplication.database .playerScoresDao().getTopScores() }) .applySchedulers() }地方applySchedulers()fun <T> Observable<T>.applySchedulers(): Observable<T> = this.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread())
noloman '17

这不适用于IntentService。因为IntentService将在其线程完成后完成。
Umang Kothari '18 -10-29

1
@UmangKothari如果打开,则不会出现异常,IntentService#onHandleIntent因为此方法在工作线程上执行,因此您不需要任何线程机制即可进行Room数据库操作
Samuel Robert

@SamuelRobert,是的,我不好。这让我无所适从。
Umang Kothari '18 -10-29

27

您不能在主线程上运行它,而应使用处理程序,异步或工作线程。此处提供示例代码,并在此处阅读有关会议室资料的文章: Android的会议室资料

/**
 *  Insert and get data using Database Async way
 */
AsyncTask.execute(new Runnable() {
    @Override
    public void run() {
        // Insert Data
        AppDatabase.getInstance(context).userDao().insert(new User(1,"James","Mathew"));

        // Get Data
        AppDatabase.getInstance(context).userDao().getAllUsers();
    }
});

如果要在主线程上运行它不是首选方法。

您可以使用此方法在主线程上实现 Room.inMemoryDatabaseBuilder()


如果我使用此方法获取数据(在这种情况下仅为getAllUsers()),如何从该方法返回数据?如果我将“返回”一词放在“运行”中,则会出现错误。
Eggakin Baconwalker

1
在某处创建接口方法并添加匿名类以从此处获取数据。
里兹万

1
这是插入/更新的最简单的解决方案。
Beer Me

12

使用lambda,可以轻松地通过AsyncTask运行

 AsyncTask.execute(() -> //run your query here );

2
方便,谢谢。顺便说一下,Kotlin更加容易:AsyncTask.execute {}
alexrnov

1
但是如何使用这种方法得到结果呢?
leeCoder

11

使用Jetbrains Anko库,您可以使用doAsync {..}方法自动执行数据库调用。这可以解决您在mcastro的答案中似乎遇到的冗长问题。

用法示例:

    doAsync { 
        Application.database.myDAO().insertUser(user) 
    }

我经常将其用于插入和更新,但是对于我建议使用RX工作流进行的选择查询。



6

您必须在后台执行请求。一种简单的方法是使用Executors

Executors.newSingleThreadExecutor().execute { 
   yourDb.yourDao.yourRequest() //Replace this by your request
}

您如何返回结果?
leeCoder

5

最好使用RxJava / Kotlin解决方案Completable.fromCallable,它将为您提供一个Observable,它不返回值,但是可以在另一个线程上进行观察和预订。

public Completable insert(Event event) {
    return Completable.fromCallable(new Callable<Void>() {
        @Override
        public Void call() throws Exception {
            return database.eventDao().insert(event)
        }
    }
}

或在科特林:

fun insert(event: Event) : Completable = Completable.fromCallable {
    database.eventDao().insert(event)
}

您可以像平常一样观察和订阅:

dataManager.insert(event)
    .subscribeOn(scheduler)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(...)

5

您可以允许在主线程上进行数据库访问,但仅出于调试目的,不应在生产环境中进行此操作。

这是原因。

注意:Room不支持在主线程上进行数据库访问,除非您在构建器上调用了allowMainThreadQueries(),因为Room可能会长时间锁定UI。异步查询(返回LiveData或Flowable实例的查询)不受此规则的约束,因为它们在需要时在后台线程上异步运行查询。


4

只需使用以下代码即可解决该问题:

Executors.newSingleThreadExecutor().execute(new Runnable() {
                    @Override
                    public void run() {
                        appDb.daoAccess().someJobes();//replace with your code
                    }
                });

或者在lambda中,您可以使用以下代码:

Executors.newSingleThreadExecutor().execute(() -> appDb.daoAccess().someJobes());

您可以appDb.daoAccess().someJobes()用自己的代码替换;


4

由于不推荐使用asyncTask,我们可以使用执行程序服务。或者,您还可以将ViewModel与LiveData一起使用,如其他答案所述。

要使用执行程序服务,您可以使用类似以下的内容。

public class DbHelper {

    private final Executor executor = Executors.newSingleThreadExecutor();

    public void fetchData(DataFetchListener dataListener){
        executor.execute(() -> {
                Object object = retrieveAgent(agentId);
                new Handler(Looper.getMainLooper()).post(() -> {
                        dataListener.onFetchDataSuccess(object);
                });
        });
    }
}

使用Main Looper,以便您可以从onFetchDataSuccess回调访问UI元素。


3

错误消息,

无法访问主线程上的数据库,因为它可能长时间锁定UI。

颇具描述性和准确性。问题是如何避免访问主线程上的数据库。那是一个巨大的话题,但要开始使用,请阅读有关AsyncTask的信息(单击此处)

- - -编辑 - - - - -

我看到您在运行单元测试时遇到问题。您有几种选择可以解决此问题:

  1. 直接在开发机器上而不是在Android设备(或仿真器)上运行测试。这适用于以数据库为中心的测试,并且实际上并不关心它们是否在设备上运行。

  2. 使用注释 @RunWith(AndroidJUnit4.class) 可以在android设备上运行测试,但不能在具有UI的活动中运行测试。在本教程中可以找到关于此的更多详细信息


我理解您的观点,我的假设是,当您尝试通过JUnit测试任何数据库操作时,同一观点同样有效。但是,在developer.android.com/topic/libraries/architecture/room.html中,示例测试方法writeUserAndReadInList不会在后台线程上调用插入查询。我在这里想念什么吗?请提出建议。
Devarshi

抱歉,我错过了这个测试存在问题的事实。我将编辑答案以添加更多信息。
戴尔·威尔逊

3

如果您对异步任务比较满意:

  new AsyncTask<Void, Void, Integer>() {
                @Override
                protected Integer doInBackground(Void... voids) {
                    return Room.databaseBuilder(getApplicationContext(),
                            AppDatabase.class, DATABASE_NAME)
                            .fallbackToDestructiveMigration()
                            .build()
                            .getRecordingDAO()
                            .getAll()
                            .size();
                }

                @Override
                protected void onPostExecute(Integer integer) {
                    super.onPostExecute(integer);
                    Toast.makeText(HomeActivity.this, "Found " + integer, Toast.LENGTH_LONG).show();
                }
            }.execute();

2

更新:当我尝试在DAO中使用@RawQuery和SupportSQLiteQuery构建查询时,我也收到此消息。

@Transaction
public LiveData<List<MyEntity>> getList(MySettings mySettings) {
    //return getMyList(); -->this is ok

    return getMyList(new SimpleSQLiteQuery("select * from mytable")); --> this is an error

解决方案:在ViewModel中构建查询并将其传递给DAO。

public MyViewModel(Application application) {
...
        list = Transformations.switchMap(searchParams, params -> {

            StringBuilder sql;
            sql = new StringBuilder("select  ... ");

            return appDatabase.rawDao().getList(new SimpleSQLiteQuery(sql.toString()));

        });
    }

要么...

您不应直接在主线程上访问数据库,例如:

 public void add(MyEntity item) {
     appDatabase.myDao().add(item); 
 }

您应该使用AsyncTask进行更新,添加和删除操作。

例:

public class MyViewModel extends AndroidViewModel {

    private LiveData<List<MyEntity>> list;

    private AppDatabase appDatabase;

    public MyViewModel(Application application) {
        super(application);

        appDatabase = AppDatabase.getDatabase(this.getApplication());
        list = appDatabase.myDao().getItems();
    }

    public LiveData<List<MyEntity>> getItems() {
        return list;
    }

    public void delete(Obj item) {
        new deleteAsyncTask(appDatabase).execute(item);
    }

    private static class deleteAsyncTask extends AsyncTask<MyEntity, Void, Void> {

        private AppDatabase db;

        deleteAsyncTask(AppDatabase appDatabase) {
            db = appDatabase;
        }

        @Override
        protected Void doInBackground(final MyEntity... params) {
            db.myDao().delete((params[0]));
            return null;
        }
    }

    public void add(final MyEntity item) {
        new addAsyncTask(appDatabase).execute(item);
    }

    private static class addAsyncTask extends AsyncTask<MyEntity, Void, Void> {

        private AppDatabase db;

        addAsyncTask(AppDatabase appDatabase) {
            db = appDatabase;
        }

        @Override
        protected Void doInBackground(final MyEntity... params) {
            db.myDao().add((params[0]));
            return null;
        }

    }
}

如果将LiveData用于选择操作,则不需要AsyncTask。


1

对于快速查询,您可以留出空间在UI线程上执行它。

AppDatabase db = Room.databaseBuilder(context.getApplicationContext(),
        AppDatabase.class, DATABASE_NAME).allowMainThreadQueries().build();

就我而言,我必须弄清楚列表中单击的用户是否存在于数据库中。如果没有,请创建用户并开始其他活动

       @Override
        public void onClick(View view) {



            int position = getAdapterPosition();

            User user = new User();
            String name = getName(position);
            user.setName(name);

            AppDatabase appDatabase = DatabaseCreator.getInstance(mContext).getDatabase();
            UserDao userDao = appDatabase.getUserDao();
            ArrayList<User> users = new ArrayList<User>();
            users.add(user);
            List<Long> ids = userDao.insertAll(users);

            Long id = ids.get(0);
            if(id == -1)
            {
                user = userDao.getUser(name);
                user.setId(user.getId());
            }
            else
            {
                user.setId(id);
            }

            Intent intent = new Intent(mContext, ChatActivity.class);
            intent.putExtra(ChatActivity.EXTRAS_USER, Parcels.wrap(user));
            mContext.startActivity(intent);
        }
    }

1

您可以使用Future和Callable。因此,您无需编写冗长的asynctask即可执行查询而无需添加allowMainThreadQueries()。

我的道查询:-

@Query("SELECT * from user_data_table where SNO = 1")
UserData getDefaultData();

我的存储库方法:-

public UserData getDefaultData() throws ExecutionException, InterruptedException {

    Callable<UserData> callable = new Callable<UserData>() {
        @Override
        public UserData call() throws Exception {
            return userDao.getDefaultData();
        }
    };

    Future<UserData> future = Executors.newSingleThreadExecutor().submit(callable);

    return future.get();
}

这就是为什么我们使用Callable / Future的原因,因为android不允许查询在主线程上运行。正如上面的qus中所问的那样
初学者

1
我的意思是,尽管答案中的代码在后台线程中进行查询,但主线程被阻塞并在查询完成时等待。因此,最终它并没有比它更好allowMainThreadQueries()。在这两种情况下,主线程均仍处于阻止状态
eugeneek '18

0

我认为正确的做法是使用RxJava将查询委托给IO线程。

我有一个刚刚遇到的等效问题的解决方案示例。

((ProgressBar) view.findViewById(R.id.progressBar_home)).setVisibility(View.VISIBLE);//Always good to set some good feedback
        Completable.fromAction(() -> {
            //Creating view model requires DB access
            homeViewModel = new ViewModelProvider(this, factory).get(HomeViewModel.class);
        }).subscribeOn(Schedulers.io())//The DB access executes on a non-main-thread thread
        .observeOn(AndroidSchedulers.mainThread())//Upon completion of the DB-involved execution, the continuation runs on the main thread
        .subscribe(
                () ->
                {
                    mAdapter = new MyAdapter(homeViewModel.getExams());
                    recyclerView.setAdapter(mAdapter);
                    ((ProgressBar) view.findViewById(R.id.progressBar_home)).setVisibility(View.INVISIBLE);
                },
                error -> error.printStackTrace()
        );

如果我们要归纳解决方案:

((ProgressBar) view.findViewById(R.id.progressBar_home)).setVisibility(View.VISIBLE);//Always good to set some good feedback
        Completable.fromAction(() -> {
            someTaskThatTakesTooMuchTime();
        }).subscribeOn(Schedulers.io())//The long task executes on a non-main-thread thread
        .observeOn(AndroidSchedulers.mainThread())//Upon completion of the DB-involved execution, the continuation runs on the main thread
        .subscribe(
                () ->
                {
                    taskIWantToDoOnTheMainThreadWhenTheLongTaskIsDone();
                },
                error -> error.printStackTrace()
        );
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.