Merge pull request #298 from w568w/master

增加了 Content Provider 接口支持
pull/300/head
kunfei 4 years ago committed by GitHub
commit 358aa1e864
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      README.md
  2. 88
      api.md
  3. 11
      app/src/androidTest/java/io/legado/app/ExampleInstrumentedTest.kt
  4. 197
      app/src/main/AndroidManifest.xml
  5. 358
      app/src/main/java/io/legado/app/api/ReaderProvider.java

@ -4,8 +4,10 @@
## 阅读3.0
书源规则 https://celeter.github.io
### 阅读API
阅读3.0 提供了2种方式的API:`Web方式`和`Content Provider方式`。您可以在[这里](api.md)根据需要自行调用。
## 免责声明
[![aVElJs.th.jpg](https://s1.ax1x.com/2020/07/28/aVElJs.th.jpg)](https://imgchr.com/i/aVElJs) [![aVEQij.th.jpg](https://s1.ax1x.com/2020/07/28/aVEQij.th.jpg)](https://imgchr.com/i/aVEQij) [![aVEudg.th.jpg](https://s1.ax1x.com/2020/07/28/aVEudg.th.jpg)](https://imgchr.com/i/aVEudg) [![aVEneS.th.jpg](https://s1.ax1x.com/2020/07/28/aVEneS.th.jpg)](https://imgchr.com/i/aVEneS) [![aVEKoQ.th.jpg](https://s1.ax1x.com/2020/07/28/aVEKoQ.th.jpg)](https://imgchr.com/i/aVEKoQ) [![aVE1Wn.th.jpg](https://s1.ax1x.com/2020/07/28/aVE1Wn.th.jpg)](https://imgchr.com/i/aVE1Wn)
## 免责声明
https://gedoor.github.io/MyBookshelf/disclaimer.html

@ -0,0 +1,88 @@
#阅读API
## 对于Web的配置
您需要先在设置中启用"Web 服务"。
## 使用
### Web
待补充
### Content Provider
#### 插入单个书源
```
URL = content://io.legado.app.api.ReaderProvider/source/insert
Method = insert
```
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
#### 插入多个书源
```
URL = content://io.legado.app.api.ReaderProvider/sources/insert
Method = insert
```
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
#### 获取书源
```
URL = content://io.legado.app.api.ReaderProvider/source/query?url=xxx
Method = query
```
获取指定URL对应的书源信息。
用`Cursor.getString(0)`取出返回结果。
#### 获取所有书源
```
URL = content://io.legado.app.api.ReaderProvider/sources/query
Method = query
```
获取APP内的所有书源。
用`Cursor.getString(0)`取出返回结果。
#### 删除多个书源
```
URL = content://io.legado.app.api.ReaderProvider/sources/delete
Method = delete
```
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
#### 插入书籍
```
URL = content://io.legado.app.api.ReaderProvider/book/insert
Method = insert
```
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
#### 获取所有书籍
```
URL = content://io.legado.app.api.ReaderProvider/books/query
Method = query
```
获取APP内的所有书籍。
用`Cursor.getString(0)`取出返回结果。
#### 获取书籍章节列表
```
URL = content://io.legado.app.api.ReaderProvider/book/chapter/query?url=xxx
Method = query
```
获取指定图书的章节列表。
用`Cursor.getString(0)`取出返回结果。
#### 获取书籍内容
```
URL = content://io.legado.app.api.ReaderProvider/book/content/query?url=xxx&index=1
Method = query
```
获取指定图书的第`index`章节的文本内容。
用`Cursor.getString(0)`取出返回结果。

@ -1,5 +1,7 @@
package io.legado.app
import android.net.Uri
import android.util.Log
import androidx.test.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
@ -16,9 +18,12 @@ import org.junit.Assert.*
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
fun testContentProvider() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getTargetContext()
assertEquals("cn.legado.book", appContext.packageName)
}
Log.d("test",
appContext.contentResolver.query(Uri.parse("content://io.legado.app.api.ReaderProvider/sources/query"),null,null,null,null)
!!.getString(0)
)
}
}

@ -25,164 +25,163 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/AppTheme.Light"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<!--主入口-->
<provider
android:name=".api.ReaderProvider"
android:authorities="io.legado.app.api.ReaderProvider"
android:enabled="true"
android:exported="true" />
<!-- 主入口 -->
<activity android:name=".ui.welcome.WelcomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--图标1-->
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity> <!-- 图标1 -->
<activity
android:name=".ui.welcome.Launcher1"
android:icon="@mipmap/launcher1"
android:enabled="false">
android:enabled="false"
android:icon="@mipmap/launcher1">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--图标2-->
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity> <!-- 图标2 -->
<activity
android:name=".ui.welcome.Launcher2"
android:icon="@mipmap/launcher2"
android:enabled="false">
android:enabled="false"
android:icon="@mipmap/launcher2">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--图标3-->
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity> <!-- 图标3 -->
<activity
android:name=".ui.welcome.Launcher3"
android:icon="@mipmap/launcher3"
android:enabled="false">
android:enabled="false"
android:icon="@mipmap/launcher3">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--图标4-->
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity> <!-- 图标4 -->
<activity
android:name=".ui.welcome.Launcher4"
android:icon="@mipmap/launcher4"
android:enabled="false">
android:enabled="false"
android:icon="@mipmap/launcher4">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--图标5-->
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity> <!-- 图标5 -->
<activity
android:name=".ui.welcome.Launcher5"
android:icon="@mipmap/launcher5"
android:enabled="false">
android:enabled="false"
android:icon="@mipmap/launcher5">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--图标6-->
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity> <!-- 图标6 -->
<activity
android:name=".ui.welcome.Launcher6"
android:icon="@mipmap/launcher6"
android:enabled="false">
android:enabled="false"
android:icon="@mipmap/launcher6">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--主界面-->
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity> <!-- 主界面 -->
<activity
android:name=".ui.main.MainActivity"
android:launchMode="singleTask"
android:alwaysRetainTaskState="true" />
<!--阅读界面-->
android:alwaysRetainTaskState="true"
android:launchMode="singleTask" /> <!-- 阅读界面 -->
<activity
android:name=".ui.book.read.ReadBookActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask" />
<!--书籍详情页-->
android:launchMode="singleTask" /> <!-- 书籍详情页 -->
<activity
android:name=".ui.book.info.BookInfoActivity"
android:launchMode="singleTop" />
<!--书籍信息编辑-->
android:launchMode="singleTop" /> <!-- 书籍信息编辑 -->
<activity
android:name="io.legado.app.ui.book.info.edit.BookInfoEditActivity"
android:launchMode="singleTask" />
<!--音频播放界面-->
android:name=".ui.book.info.edit.BookInfoEditActivity"
android:launchMode="singleTask" /> <!-- 音频播放界面 -->
<activity
android:name=".ui.audio.AudioPlayActivity"
android:launchMode="singleTask" />
<!--授权界面-->
android:launchMode="singleTask" /> <!-- 授权界面 -->
<activity
android:name=".help.permission.PermissionActivity"
android:theme="@style/Activity.Permission" />
<!--二维码扫描-->
android:theme="@style/Activity.Permission" /> <!-- 二维码扫描 -->
<activity
android:name=".ui.qrcode.QrCodeActivity"
android:launchMode="singleTask" />
<!--书源编辑-->
android:launchMode="singleTask" /> <!-- 书源编辑 -->
<activity
android:name=".ui.book.source.edit.BookSourceEditActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
<!--订阅源编辑-->
android:windowSoftInputMode="adjustResize|stateHidden" /> <!-- 订阅源编辑 -->
<activity
android:name=".ui.rss.source.edit.RssSourceEditActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
<!--配置界面-->
android:windowSoftInputMode="adjustResize|stateHidden" /> <!-- 配置界面 -->
<activity
android:name=".ui.config.ConfigActivity"
android:launchMode="singleTask" />
<!--搜索界面-->
android:launchMode="singleTask" /> <!-- 搜索界面 -->
<activity
android:name="io.legado.app.ui.book.search.SearchActivity"
android:launchMode="singleTask" />
<!--关于界面-->
android:name=".ui.book.search.SearchActivity"
android:launchMode="singleTask" /> <!-- 关于界面 -->
<activity
android:name=".ui.about.AboutActivity"
android:launchMode="singleTask" />
<!--捐赠界面-->
android:launchMode="singleTask" /> <!-- 捐赠界面 -->
<activity
android:name=".ui.about.DonateActivity"
android:launchMode="singleTask" />
<!--书源管理-->
android:launchMode="singleTask" /> <!-- 书源管理 -->
<activity
android:name=".ui.book.source.manage.BookSourceActivity"
android:launchMode="singleTop">
@ -196,8 +195,7 @@
android:host="booksource"
android:scheme="yuedu" />
</intent-filter>
</activity>
<!--订阅源管理-->
</activity> <!-- 订阅源管理 -->
<activity
android:name=".ui.rss.source.manage.RssSourceActivity"
android:launchMode="singleTop">
@ -211,8 +209,7 @@
android:host="rsssource"
android:scheme="yuedu" />
</intent-filter>
</activity>
<!--替换规则界面-->
</activity> <!-- 替换规则界面 -->
<activity
android:name=".ui.replacerule.ReplaceRuleActivity"
android:launchMode="singleTop">
@ -226,67 +223,59 @@
android:host="replace"
android:scheme="yuedu" />
</intent-filter>
</activity>
<!--书籍管理-->
</activity> <!-- 书籍管理 -->
<activity
android:name=".ui.book.arrange.ArrangeBookActivity"
android:launchMode="singleTop" />
<!--书源调试-->
android:launchMode="singleTop" /> <!-- 书源调试 -->
<activity
android:name=".ui.book.source.debug.BookSourceDebugActivity"
android:launchMode="singleTop" />
<!--目录-->
android:launchMode="singleTop" /> <!-- 目录 -->
<activity
android:name="io.legado.app.ui.book.chapterlist.ChapterListActivity"
android:screenOrientation="behind"
android:launchMode="singleTop" />
<!--RSS条目-->
android:name=".ui.book.chapterlist.ChapterListActivity"
android:launchMode="singleTop"
android:screenOrientation="behind" /> <!-- RSS条目 -->
<activity
android:name=".ui.rss.article.RssSortActivity"
android:launchMode="singleTop" />
<!--RSS阅读-->
android:launchMode="singleTop" /> <!-- RSS阅读 -->
<activity
android:name=".ui.rss.read.ReadRssActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true"
android:launchMode="singleTop" />
<!--导入书籍-->
android:launchMode="singleTop" /> <!-- 导入书籍 -->
<activity
android:name="io.legado.app.ui.book.local.ImportBookActivity"
android:launchMode="singleTop" />
<!--发现界面-->
android:name=".ui.book.local.ImportBookActivity"
android:launchMode="singleTop" /> <!-- 发现界面 -->
<activity
android:name="io.legado.app.ui.book.explore.ExploreShowActivity"
android:launchMode="singleTop" />
<!--订阅源调试-->
android:name=".ui.book.explore.ExploreShowActivity"
android:launchMode="singleTop" /> <!-- 订阅源调试 -->
<activity
android:name=".ui.rss.source.debug.RssSourceDebugActivity"
android:launchMode="singleTop" />
<!--Rss收藏-->
android:launchMode="singleTop" /> <!-- Rss收藏 -->
<activity
android:name=".ui.rss.favorites.RssFavoritesActivity"
android:launchMode="singleTop" />
<!--下载界面-->
android:launchMode="singleTop" /> <!-- 下载界面 -->
<activity
android:name="io.legado.app.ui.book.download.DownloadActivity"
android:launchMode="singleTop" />
<!--书源登录-->
android:name=".ui.book.download.DownloadActivity"
android:launchMode="singleTop" /> <!-- 书源登录 -->
<activity
android:name=".ui.login.SourceLogin"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" />
<!--文字处理-->
android:hardwareAccelerated="true" /> <!-- 文字处理 -->
<activity
android:name=".receiver.SharedReceiverActivity"
android:label="@string/receiving_shared_label">
<intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>

@ -0,0 +1,358 @@
/*
* Copyright (C) 2020 w568w
*/
package io.legado.app.api;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.CharArrayBuffer;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import com.google.gson.Gson;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import io.legado.app.web.controller.BookshelfController;
import io.legado.app.web.controller.SourceController;
import io.legado.app.web.utils.ReturnData;
/**
* Export book data to other app.
*/
public class ReaderProvider extends ContentProvider {
private enum RequestCode {
saveSource, saveSources, saveBook, deleteSources, getSource, getSources, getBookshelf, getChapterList, getBookContent
}
public static final String POST_BODY_KEY = "json";
public static final String AUTHORITY = "io.legado.app.api.ReaderProvider";
private static UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private SourceController mSourceController;
private BookshelfController mBookshelfController;
static {
sMatcher.addURI(AUTHORITY, "source/insert", RequestCode.saveSource.ordinal());
sMatcher.addURI(AUTHORITY, "sources/insert", RequestCode.saveSources.ordinal());
sMatcher.addURI(AUTHORITY, "book/insert", RequestCode.saveBook.ordinal());
sMatcher.addURI(AUTHORITY, "sources/delete", RequestCode.deleteSources.ordinal());
sMatcher.addURI(AUTHORITY, "source/query", RequestCode.getSource.ordinal());
sMatcher.addURI(AUTHORITY, "sources/query", RequestCode.getSources.ordinal());
sMatcher.addURI(AUTHORITY, "books/query", RequestCode.getBookshelf.ordinal());
sMatcher.addURI(AUTHORITY, "book/chapter/query", RequestCode.getChapterList.ordinal());
sMatcher.addURI(AUTHORITY, "book/content/query", RequestCode.getBookContent.ordinal());
}
public ReaderProvider() {
}
@Override
public int delete(@NotNull Uri uri, String selection, String[] selectionArgs) {
if (sMatcher.match(uri) < 0)
return -1;
switch (RequestCode.values()[sMatcher.match(uri)]) {
case deleteSources:
mSourceController.deleteSources(selection);
break;
default:
throw new IllegalStateException("Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name());
}
return 0;
}
@Override
public String getType(@NotNull Uri uri) {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public Uri insert(@NotNull Uri uri, ContentValues values) {
if (sMatcher.match(uri) < 0)
return null;
switch (RequestCode.values()[sMatcher.match(uri)]) {
case saveSource:
mSourceController.saveSource(values.getAsString(POST_BODY_KEY));
break;
case saveBook:
mBookshelfController.saveBook(values.getAsString(POST_BODY_KEY));
case saveSources:
mSourceController.saveSources(values.getAsString(POST_BODY_KEY));
default:
throw new IllegalStateException("Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name());
}
return null;
}
@Override
public boolean onCreate() {
mSourceController = new SourceController();
mBookshelfController = new BookshelfController();
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Map<String, ArrayList<String>> map = new HashMap<>();
ArrayList<String> list = new ArrayList<>();
list.add(uri.getQueryParameter("url"));
map.put("url", list);
list = new ArrayList<>();
list.add(uri.getQueryParameter("index"));
map.put("index", list);
if (sMatcher.match(uri) < 0)
return null;
switch (RequestCode.values()[sMatcher.match(uri)]) {
case getSource:
return new SimpleCursor(mSourceController.getSource(map));
case getSources:
return new SimpleCursor(mSourceController.getSources());
case getBookshelf:
return new SimpleCursor(mBookshelfController.getBookshelf());
case getBookContent:
return new SimpleCursor(mBookshelfController.getBookContent(map));
case getChapterList:
return new SimpleCursor(mBookshelfController.getChapterList(map));
default:
throw new IllegalStateException("Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name());
}
}
@Override
public int update(@NotNull Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
throw new UnsupportedOperationException("Not yet implemented");
}
/**
* Simple inner class to deliver json callback data.
*
* Only getString() makes sense.
*/
private static class SimpleCursor implements Cursor {
private String mData;
public SimpleCursor(ReturnData data) {
this.mData = new Gson().toJson(data);
}
@Override
public int getCount() {
return 1;
}
@Override
public int getPosition() {
return 0;
}
@Override
public boolean move(int i) {
return true;
}
@Override
public boolean moveToPosition(int i) {
return true;
}
@Override
public boolean moveToFirst() {
return true;
}
@Override
public boolean moveToLast() {
return true;
}
@Override
public boolean moveToNext() {
return true;
}
@Override
public boolean moveToPrevious() {
return true;
}
@Override
public boolean isFirst() {
return true;
}
@Override
public boolean isLast() {
return true;
}
@Override
public boolean isBeforeFirst() {
return false;
}
@Override
public boolean isAfterLast() {
return false;
}
@Override
public int getColumnIndex(String s) {
return 0;
}
@Override
public int getColumnIndexOrThrow(String s) throws IllegalArgumentException {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public String getColumnName(int i) {
return null;
}
@Override
public String[] getColumnNames() {
return new String[0];
}
@Override
public int getColumnCount() {
return 0;
}
@Override
public byte[] getBlob(int i) {
return new byte[0];
}
@Override
public String getString(int i) {
return mData;
}
@Override
public void copyStringToBuffer(int i, CharArrayBuffer charArrayBuffer) {
}
@Override
public short getShort(int i) {
return 0;
}
@Override
public int getInt(int i) {
return 0;
}
@Override
public long getLong(int i) {
return 0;
}
@Override
public float getFloat(int i) {
return 0;
}
@Override
public double getDouble(int i) {
return 0;
}
@Override
public int getType(int i) {
return 0;
}
@Override
public boolean isNull(int i) {
return false;
}
@Override
public void deactivate() {
}
@Override
public boolean requery() {
return false;
}
@Override
public void close() {
}
@Override
public boolean isClosed() {
return false;
}
@Override
public void registerContentObserver(ContentObserver contentObserver) {
}
@Override
public void unregisterContentObserver(ContentObserver contentObserver) {
}
@Override
public void registerDataSetObserver(DataSetObserver dataSetObserver) {
}
@Override
public void unregisterDataSetObserver(DataSetObserver dataSetObserver) {
}
@Override
public void setNotificationUri(ContentResolver contentResolver, Uri uri) {
}
@Override
public Uri getNotificationUri() {
return null;
}
@Override
public boolean getWantsAllOnMoveCalls() {
return false;
}
@Override
public void setExtras(Bundle bundle) {
}
@Override
public Bundle getExtras() {
return null;
}
@Override
public Bundle respond(Bundle bundle) {
return null;
}
}
}
Loading…
Cancel
Save