• Android TV : 电视输入框架(TIF)剖析


      Android TIF(Android TV input Framework)是Google向电视制造商提供了一套标准的API,用于创建Input模块来控制Android电视。这套API的底层实现的原理是aidl和provider,从而进行了跨进程通信。系统或第三方的应用可以通过TIF获得所有输入(input)的信源(输入的模块包括:搜台模块,MDMI模块,网络模块等),然后通过aidl切台输出到屏幕上。
     
    1.电视相关的知识:
        HDMI:高清晰度多媒体接口(英文:High Definition Multimedia Interface,HDMI)是一种数字化视频/音频接口技术,是适合影像传输的专用型数字化接口。
        IPTV:网络电视,也叫VOD电视,三方比如说某某视频公司提供的视频资源在电视上播放。
        DTV:数字电视
        ATV:模拟电视
    2.TIF的组成部分:
        1)TV Provider (com.android.providers.tv.TvProvider):一个包含频道、节目和相关权限的数据库。
        2)TV App (com.android.tv.TvActivity):一个和用户交互的系统应用。
        3)TV Input Manager (android.media.tv.TvInputManager):一个中间接口层,能够让TV Inputs和TV App进行通讯。
        4)TV Input:可以看做是一个代表物理或者虚拟的电视接收器或者输入端口的应用。Input在TIF中可以看做是一个输入源。
        5)TV Input HAL (tv_input module):TV Input的硬件抽象层,可以让系统的TV inputs访问TV特有硬件。
        6)Parental Control:儿童锁,一种可以锁住某些频道和节目的技术。
        7)HDMI-CEC:一种可以通过HDMI在多种设备上进行远程控制的技术。CEC(Consumer Electronics Control消费电子控制)

    3.TIF官方流程图:

        

      如上图所示,TVProvider和TV Input Manager就是TIF中的内容,liveTVApp通过turning调用TV Input Manager获得一个session,session里面放的是一路信源的状态,TV Input Manager 必须与 TV Input 创建一对一的会话。liveTVApp通过session以aidl的方式调用TVinputService获得相关的频道和具体的节目信息进行播放,并提供家长控制功能。TvInput将获得的Channel和Programs信息写入到/data/data/com.android.providers.tv/databases/tv.db数据库中。

    4.TIF为开发者提供的接口
    1)TvView:负责显示播放的内容。它是一个ViewGroup的子类,它是切台的入口,内置surface用于显示视频播放的内容和通过控制session可以控制音量的大小等。
    2)TvInputService:TvInputService是一个重要的类,继承了它并实现一些规范就可以实现一路input信源供其它应用使用。在该service中要实现onCreatSession()方法该方法要返回一个TvInputService.Session对象。这里的service在Manifest中定义时要注意要添加permission和action。添加完之后系统的TvInputManager可以检测到该service是一个TvInputService,也就是一路信源。
      下面创建一个自定义的Service,而这个Service要继承系统的TvInputService,当然为了简化这个过程我们可以使用android官方提供的TIF 随播内容库:
    compile 'com.google.android.libraries.tv:companionlibrary:0.2'
    public class TvService extends BaseTvInputService {
        @Nullable
        @Override
        public TvInputService.Session onCreateSession(@NonNull String inputId) {
            TvInputSessionImpl session = new TvInputSessionImpl(this, inputId);
            session.setOverlayViewEnabled(true);
            return session;
        }
    }

    这里的BaseTvInputService也是继承的TvInputService,需要复写onCreateSession方法,创建自己的Session用于和TvInputManager交互,最后在清单文件中配置如下:

    <service
      android:name=".service.TvService"
      android:permission="android.permission.BIND_TV_INPUT">  <intent-filter>    <action android:name="android.media.tv.TvInputService" />  </intent-filter>  <meta-data
        android:name="android.media.tv.input"
        android:resource="@xml/richtvinputservice" /></service>

    接着在xml/richtvinputservice中配置了两个activty,这个是提供LiveTv去打开的,比如第一次启动这个源时,需要启动到setupActivity所指定的activity,设置时需要启动到settingsActivity配置的activity. 

    <?xml version="1.0" encoding="utf-8"?>
    <tv-input xmlns:android="http://schemas.android.com/apk/res/android"
        android:settingsActivity="com.xray.tv.input.MainActivity"
        android:setupActivity="com.xray.tv.input.MainActivity" />
    3)TvInputService.Sssion:该session类TvView通过Tune方法会指定相应的inputId(往往是该service对应的“包名/.类名”)和uri,uri中包含对应的节目id,该tune方法会调用Session的Onturn方法中,在这个方法中解析传过来的id,根据id利用TvProvider去查询数据库的数据,设置给player,这里使用onSetSurface()方法将TvView创建的surface设置给player,然后player就在该surface上显示内容。
    4)TvContract:介于TvProvider和TvApp之间的一层封装,它里面封装了一些uri。里面有两个内部类是两个javaBean。他们分别是TvContract.channels(频道表),TvContract.Programs(频道里面的节目单,比如少儿频道里面海贼王第5集,火影忍者第6集等)。
    5)TvInputManager:这个是TIF的核心类,它是系统的类,可以监测到在系统的service中注册"android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS"action的类,并将其设为一路信源。它来管理一些回调,比如video是否可用,video的大小尺寸是否变换。通过下面的代码可以获得一个TvInputManager:
    TvInputManager tvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
      TvInputManager只是我们当前进程的代理,它的真正实现其实是一个系统的Service,所以我们可以知道这个service其实在system_server进程中,在类TvInputManagerService中实现。由于这个地方是跨进程通信,其实它使用的是aidl的方式,所以我们可以找到TvInputManager在aidl中定义的接口:
    /**
     * Interface to the TV input manager service.
     * @hide
     */
    interface ITvInputManager {
        List<TvInputInfo> getTvInputList(int userId);
        TvInputInfo getTvInputInfo(in String inputId, int userId);
        void updateTvInputInfo(in TvInputInfo inputInfo, int userId);
        int getTvInputState(in String inputId, int userId);
    
        List<TvContentRatingSystemInfo> getTvContentRatingSystemList(int userId);
    
        void registerCallback(in ITvInputManagerCallback callback, int userId);
        void unregisterCallback(in ITvInputManagerCallback callback, int userId);
    
        boolean isParentalControlsEnabled(int userId);
        void setParentalControlsEnabled(boolean enabled, int userId);
        boolean isRatingBlocked(in String rating, int userId);
        List<String> getBlockedRatings(int userId);
        void addBlockedRating(in String rating, int userId);
        void removeBlockedRating(in String rating, int userId);
    
        void createSession(in ITvInputClient client, in String inputId, boolean isRecordingSession,
                int seq, int userId);
        void releaseSession(in IBinder sessionToken, int userId);
        int getClientPid(in String sessionId);
    
        void setMainSession(in IBinder sessionToken, int userId);
        void setSurface(in IBinder sessionToken, in Surface surface, int userId);
        void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height,
                int userId);
        void setVolume(in IBinder sessionToken, float volume, int userId);
        void tune(in IBinder sessionToken, in Uri channelUri, in Bundle params, int userId);
        void setCaptionEnabled(in IBinder sessionToken, boolean enabled, int userId);
        void selectTrack(in IBinder sessionToken, int type, in String trackId, int userId);
    
        void sendAppPrivateCommand(in IBinder sessionToken, in String action, in Bundle data,
                int userId);
    
        void createOverlayView(in IBinder sessionToken, in IBinder windowToken, in Rect frame,
                int userId);
        void relayoutOverlayView(in IBinder sessionToken, in Rect frame, int userId);
        void removeOverlayView(in IBinder sessionToken, int userId);
    
        void unblockContent(in IBinder sessionToken, in String unblockedRating, int userId);
    
        void timeShiftPlay(in IBinder sessionToken, in Uri recordedProgramUri, int userId);
        void timeShiftPause(in IBinder sessionToken, int userId);
        void timeShiftResume(in IBinder sessionToken, int userId);
        void timeShiftSeekTo(in IBinder sessionToken, long timeMs, int userId);
        void timeShiftSetPlaybackParams(in IBinder sessionToken, in PlaybackParams params, int userId);
        void timeShiftEnablePositionTracking(in IBinder sessionToken, boolean enable, int userId);
    
        // For the recording session
        void startRecording(in IBinder sessionToken, in Uri programUri, in Bundle params, int userId);
        void stopRecording(in IBinder sessionToken, int userId);
    
        // For TV input hardware binding
        List<TvInputHardwareInfo> getHardwareList();
        ITvInputHardware acquireTvInputHardware(int deviceId, in ITvInputHardwareCallback callback,
                in TvInputInfo info, int userId, String tvInputSessionId, int priorityHint);
        void releaseTvInputHardware(int deviceId, in ITvInputHardware hardware, int userId);
    
        // For TV input capturing
        List<TvStreamConfig> getAvailableTvStreamConfigList(in String inputId, int userId);
        boolean captureFrame(in String inputId, in Surface surface, in TvStreamConfig config,
                int userId);
        boolean isSingleSessionActive(int userId);
    
        // For DVB device binding
        List<DvbDeviceInfo> getDvbDeviceList();
        ParcelFileDescriptor openDvbDevice(in DvbDeviceInfo info, int device);
    
        // For preview channels and programs
        void sendTvInputNotifyIntent(in Intent intent, int userId);
        void requestChannelBrowsable(in Uri channelUri, int userId);
    
        // For CTS purpose only. Add/remove a TvInputHardware device
        void addHardwareDevice(in int deviceId);
        void removeHardwareDevice(in int deviceId);
    }

    它的实现是在TvInputManagerService的内部类BinderService中:

        private final class BinderService extends ITvInputManager.Stub {
            @Override
            public List<TvInputInfo> getTvInputList(int userId) {
                final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
                        Binder.getCallingUid(), userId, "getTvInputList");
                final long identity = Binder.clearCallingIdentity();
                try {
                    synchronized (mLock) {
                        UserState userState = getOrCreateUserStateLocked(resolvedUserId);
                        List<TvInputInfo> inputList = new ArrayList<>();
                        for (TvInputState state : userState.inputMap.values()) {
                            inputList.add(state.info);
                        }
                        return inputList;
                    }
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }
    
            @Override
            public TvInputInfo getTvInputInfo(String inputId, int userId) {
                final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
                        Binder.getCallingUid(), userId, "getTvInputInfo");
                final long identity = Binder.clearCallingIdentity();
                try {
                    synchronized (mLock) {
                        UserState userState = getOrCreateUserStateLocked(resolvedUserId);
                        TvInputState state = userState.inputMap.get(inputId);
                        return state == null ? null : state.info;
                    }
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }
    
        ......
        }

    TvInputManagerService是在SystemServer中启动的,具体在SystemServer类的startOtherServices方法中:

                if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_LIVE_TV)
                        || mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
                    t.traceBegin("StartTvInputManager");
                    mSystemServiceManager.startService(TvInputManagerService.class);
                    t.traceEnd();
                }

    注意上面会判断系统TV和LEANBACK的特征而决定是否启动TvInputManagerService,特征可在device/google/atv/permissions/tv_core_hardware.xml中进行配置:

    <?xml version="1.0" encoding="utf-8"?>
    <!-- Copyright (C) 2014 The Android Open Source Project
    
         Licensed under the Apache License, Version 2.0 (the "License");
         you may not use this file except in compliance with the License.
         You may obtain a copy of the License at
    
             http://www.apache.org/licenses/LICENSE-2.0
    
         Unless required by applicable law or agreed to in writing, software
         distributed under the License is distributed on an "AS IS" BASIS,
         WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         See the License for the specific language governing permissions and
         limitations under the License.
    -->
    
    <permissions>
    
        <!-- These are the hardware components that all television devices must
             include.  Devices with optional hardware must also include extra hardware
             files.
        -->
        <feature name="android.hardware.audio.output" />
        <feature name="android.hardware.location" />
        <feature name="android.hardware.location.network" />
        <feature name="android.hardware.screen.landscape" />
        <feature name="android.hardware.type.television" />
    
        <feature name="android.software.backup" />
        <feature name="android.software.leanback" />
        <feature name="android.software.leanback_only" />
        <feature name="android.software.live_tv" />
        <feature name="android.software.picture_in_picture" notLowRam="true" />
        <feature name="android.software.activities_on_secondary_displays" notLowRam="true" />
        <feature name="android.software.voice_recognizers" notLowRam="true" />
        <feature name="android.software.input_methods" />
        <feature name="android.software.autofill" />
    
        <feature name="android.software.cts" />
    
    </permissions>
    得到TvInputManager后我们可以遍历拿到系统当前有多少个service是Tv信源:
    List<TvInputInfo> list = tvInputManager.getTvInputList();
      for(TvInputInfo info:list){    
      Log.i(TAG, "id:" + info.getId());
    }
    6) TvInputInfo:TvInput的信息。包括频道类型,图标,名称等信息。
      因为TvInput经常是以第三方应用的方式实现的,当TvInput应用安装时,TvInputManagerService会检测安装包中是否包含TvInputService。
    private void registerBroadcastReceivers() {
            PackageMonitor monitor = new PackageMonitor() {
                private void buildTvInputList(String[] packages) {
                    synchronized (mLock) {
                        if (mCurrentUserId == getChangingUserId()) {
                            buildTvInputListLocked(mCurrentUserId, packages);
                            buildTvContentRatingSystemListLocked(mCurrentUserId);
                        }
                    }
                }
     
                @Override
                public void onPackageUpdateFinished(String packageName, int uid) {
                    if (DEBUG) Slog.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")");
                    // This callback is invoked when the TV input is reinstalled.
                    // In this case, isReplacing() always returns true.
                    buildTvInputList(new String[] { packageName });
                }
     
              ...
        }

    当有安装包安装时,监测其中是否有TvInputService,并且权限符合则绑定这个Service.

    private void buildTvInputListLocked(int userId, String[] updatedPackages) {
            UserState userState = getOrCreateUserStateLocked(userId);
            userState.packageSet.clear();
     
            if (DEBUG) Slog.d(TAG, "buildTvInputList");
            PackageManager pm = mContext.getPackageManager();
            List<ResolveInfo> services = pm.queryIntentServicesAsUser(
                    new Intent(TvInputService.SERVICE_INTERFACE),
                    PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
                    userId);
            List<TvInputInfo> inputList = new ArrayList<>();
            for (ResolveInfo ri : services) {
                ServiceInfo si = ri.serviceInfo;
                //检测是否有android.permission.BIND_TV_INPUT这个权限
                if (!android.Manifest.permission.BIND_TV_INPUT.equals(si.permission)) {
                    Slog.w(TAG, "Skipping TV input " + si.name + ": it does not require the permission "
                            + android.Manifest.permission.BIND_TV_INPUT);
                    continue;
                }
     
                ComponentName component = new ComponentName(si.packageName, si.name);
                if (hasHardwarePermission(pm, component)) {
                    ServiceState serviceState = userState.serviceStateMap.get(component);
                    if (serviceState == null) {
                        // New hardware input found. Create a new ServiceState and connect to the
                        // service to populate the hardware list.
                        serviceState = new ServiceState(component, userId);
                        userState.serviceStateMap.put(component, serviceState);
                        updateServiceConnectionLocked(component, userId);
                    } else {
                        inputList.addAll(serviceState.hardwareInputMap.values());
                    }
                } else {
                    try {
                        TvInputInfo info = new TvInputInfo.Builder(mContext, ri).build();
                        inputList.add(info);
                    } catch (Exception e) {
                        Slog.e(TAG, "failed to load TV input " + si.name, e);
                        continue;
                    }
                }
                userState.packageSet.add(si.packageName);
            }
     
            Map<String, TvInputState> inputMap = new HashMap<>();
            for (TvInputInfo info : inputList) {
                if (DEBUG) {
                    Slog.d(TAG, "add " + info.getId());
                }
                TvInputState inputState = userState.inputMap.get(info.getId());
                if (inputState == null) {
                    inputState = new TvInputState();
                }
                inputState.info = info;
                inputMap.put(info.getId(), inputState);
            }
     
            for (String inputId : inputMap.keySet()) {
                if (!userState.inputMap.containsKey(inputId)) {
                    notifyInputAddedLocked(userState, inputId);
                } else if (updatedPackages != null) {
                    // Notify the package updates
                    ComponentName component = inputMap.get(inputId).info.getComponent();
                    for (String updatedPackage : updatedPackages) {
                        if (component.getPackageName().equals(updatedPackage)) {
                            //绑定TvInputService
                            updateServiceConnectionLocked(component, userId);
                            notifyInputUpdatedLocked(userState, inputId);
                            break;
                        }
                    }
                }
            }
            ...
        }

    绑定第三方自定义的TvInputService:

    private void updateServiceConnectionLocked(ComponentName component, int userId) {
            UserState userState = getOrCreateUserStateLocked(userId);
            ServiceState serviceState = userState.serviceStateMap.get(component);
            if (serviceState == null) {
                return;
            }
            if (serviceState.reconnecting) {
                if (!serviceState.sessionTokens.isEmpty()) {
                    // wait until all the sessions are removed.
                    return;
                }
                serviceState.reconnecting = false;
            }
     
            boolean shouldBind;
            if (userId == mCurrentUserId) {
                shouldBind = !serviceState.sessionTokens.isEmpty() || serviceState.isHardware;
            } else {
                // For a non-current user,
                // if sessionTokens is not empty, it contains recording sessions only
                // because other sessions must have been removed while switching user
                // and non-recording sessions are not created by createSession().
                shouldBind = !serviceState.sessionTokens.isEmpty();
            }
     
            if (serviceState.service == null && shouldBind) {
                // This means that the service is not yet connected but its state indicates that we
                // have pending requests. Then, connect the service.
                if (serviceState.bound) {
                    // We have already bound to the service so we don't try to bind again until after we
                    // unbind later on.
                    return;
                }
                if (DEBUG) {
                    Slog.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")");
                }
                //bind 第三方应用自定义的TvInputService
                Intent i = new Intent(TvInputService.SERVICE_INTERFACE).setComponent(component);
                serviceState.bound = mContext.bindServiceAsUser(
                        i, serviceState.connection,
                        Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
                        new UserHandle(userId));
            } else if (serviceState.service != null && !shouldBind) {
                // This means that the service is already connected but its state indicates that we have
                // nothing to do with it. Then, disconnect the service.
                if (DEBUG) {
                    Slog.d(TAG, "unbindService(service=" + component + ")");
                }
                mContext.unbindService(serviceState.connection);
                userState.serviceStateMap.remove(component);
            }
        }

    TvInputService现在绑定了,那么TvInputMangerService和TvInputService交互的逻辑就到了ServiceConnection中,它的实现在InputServiceConnection中:

            @Override
            public void onServiceConnected(ComponentName component, IBinder service) {
                if (DEBUG) {
                    Slog.d(TAG, "onServiceConnected(component=" + component + ")");
                }
                synchronized (mLock) {
                    UserState userState = mUserStates.get(mUserId);
                    if (userState == null) {
                        // The user was removed while connecting.
                        mContext.unbindService(this);
                        return;
                    }
                    ServiceState serviceState = userState.serviceStateMap.get(mComponent);
                    serviceState.service = ITvInputService.Stub.asInterface(service);
    
                    // Register a callback, if we need to.
                    if (serviceState.isHardware && serviceState.callback == null) {
                        serviceState.callback = new ServiceCallback(mComponent, mUserId);
                        try {
                            serviceState.service.registerCallback(serviceState.callback);
                        } catch (RemoteException e) {
                            Slog.e(TAG, "error in registerCallback", e);
                        }
                    }
    
                    List<IBinder> tokensToBeRemoved = new ArrayList<>();
    
                    // And create sessions, if any.
                    for (IBinder sessionToken : serviceState.sessionTokens) {
                        if (!createSessionInternalLocked(serviceState.service, sessionToken, mUserId)) {
                            tokensToBeRemoved.add(sessionToken);
                        }
                    }
    
                    for (IBinder sessionToken : tokensToBeRemoved) {
                        removeSessionStateLocked(sessionToken, mUserId);
                    }
    
                    for (TvInputState inputState : userState.inputMap.values()) {
                        if (inputState.info.getComponent().equals(component)
                                && inputState.state != INPUT_STATE_CONNECTED) {
                            notifyInputStateChangedLocked(userState, inputState.info.getId(),
                                    inputState.state, null);
                        }
                    }
    
                    if (serviceState.isHardware) {
                        serviceState.hardwareInputMap.clear();
                        for (TvInputHardwareInfo hardware : mTvInputHardwareManager.getHardwareList()) {
                            try {
                                serviceState.service.notifyHardwareAdded(hardware);
                            } catch (RemoteException e) {
                                Slog.e(TAG, "error in notifyHardwareAdded", e);
                            }
                        }
                        for (HdmiDeviceInfo device : mTvInputHardwareManager.getHdmiDeviceList()) {
                            try {
                                serviceState.service.notifyHdmiDeviceAdded(device);
                            } catch (RemoteException e) {
                                Slog.e(TAG, "error in notifyHdmiDeviceAdded", e);
                            }
                        }
                    }
                }
            }

      在onServiceConnected成功后,就可以拿到从TvInputService中获取的Binder对象,和第三方的TvInputService联通进行交互,它们之间交互需要创建一个Session,也就是TvInputService.Session,这个Session中的交互是通过ITvInputSessionCallback来实现。

    7)TvInputCallback:TvView的一个内部类,TvInputCallBack可以反馈给TvView一些信息比如连接service是否成功,Video是否可用等:
    ITvInputSessionCallback.aidl
    /**
     * Helper interface for ITvInputSession to allow the TV input to notify the system service when a
     * new session has been created.
     * @hide
     */
    oneway interface ITvInputSessionCallback {
        void onSessionCreated(ITvInputSession session, in IBinder hardwareSessionToken);
        void onSessionEvent(in String name, in Bundle args);
        void onChannelRetuned(in Uri channelUri);
        void onTracksChanged(in List<TvTrackInfo> tracks);
        void onTrackSelected(int type, in String trackId);
        void onVideoAvailable();
        void onVideoUnavailable(int reason);
        void onContentAllowed();
        void onContentBlocked(in String rating);
        void onLayoutSurface(int left, int top, int right, int bottom);
        void onTimeShiftStatusChanged(int status);
        void onTimeShiftStartPositionChanged(long timeMs);
        void onTimeShiftCurrentPositionChanged(long timeMs);
    
        // For the recording session
        void onTuned(in Uri channelUri);
        void onRecordingStopped(in Uri recordedProgramUri);
        void onError(int error);
    }

    自定义第三方TvInputService时,根据需求实现以上方法:

    tvView.setCallback(new TvView.TvInputCallback() {    
        @Override    
        public void onConnectionFailed(String inputId) {
             super.onConnectionFailed(inputId);
             LogUtil.i(this,"MainActivity.onConnectionFailed:"+inputId); 
        }
        @Override    
        public void onDisconnected(String inputId) { 
            super.onDisconnected(inputId);
             LogUtil.i(this,"MainActivity.onDisconnected."); 
        }    
       @Override    
       public void onVideoSizeChanged(String inputId, int width, int height) { 
            super.onVideoSizeChanged(inputId, width, height); 
            LogUtil.i(this,"MainActivity.onVideoSizeChanged.");    
       }    
       @Override
         public void onVideoAvailable(String inputId) {
            super.onVideoAvailable(inputId);
            LogUtil.i(this,"MainActivity.onVideoAvailable.inputId:"+inputId);
        }
        @Override
        public void onVideoUnavailable(String inputId, int reason) {
            super.onVideoUnavailable(inputId, reason);
            LogUtil.i(this,"MainActivity.onVideoUnavailable.");
        }    
    ......
    });

    至此,TvInputManager和第三方TvInputService的交互就完成了。

    8)TvProvider:

    LiveTv和TvInput之间交互还有一种方式就是TvProvider, TvInput应用会将自己的频道和节目数据写入TvProvider对应的数据库中,数据库的位置在:/data/data/com.android.providers.tv/databases/tv.db

    这样LiveTv就可以读取TvProvider中的数据了。当然这里的数据除了LiveTv和当前的TvInput应用,其他应用是没有权限读取的。

  • 相关阅读:
    python float转为decimal
    python 断言大全
    python如何判断一个字符串是中文,还是英文。
    分享:selenium(一) xpath
    接口测试——带token请求post接口(postman学习)
    git stash命令
    我的爹娘(一)
    appium自动化测试 环境搭建
    linux下的定时任务
    php面向对象3
  • 原文地址:https://www.cnblogs.com/blogs-of-lxl/p/15055473.html
Copyright © 2020-2023  润新知