Android13 勿扰模式

简介: 学习笔记

                   break;

               case Settings.Secure.ZEN_DURATION_FOREVER:

                   mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);

                   break;

               default:

                   Uri conditionId = ZenModeConfig.toTimeCondition(mContext, zenDuration,

                           mHost.getUserId(), true).id;

                   mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,

                           conditionId, TAG);

           }

       }

   }

}

@SysUISingleton

public class ZenModeControllerImpl extends CurrentUserTracker

       implements ZenModeController, Dumpable {

       @Override

   public void setZen(int zen, Uri conditionId, String reason) {

       mNoMan.setZenMode(zen, conditionId, reason);

   }

}

public class NotificationManagerService extends SystemService {

       @VisibleForTesting

   final IBinder mService = new INotificationManager.Stub() {

       @Override

       public void setZenMode(int mode, Uri conditionId, String reason) throws RemoteException {

           enforceSystemOrSystemUI("INotificationManager.setZenMode");

           final long identity = Binder.clearCallingIdentity();

           try {

               mZenModeHelper.setManualZenMode(mode, conditionId, null, reason);

           } finally {

               Binder.restoreCallingIdentity(identity);

           }

       }

   }    

}

public class ZenModeHelper {

       public void setManualZenMode(int zenMode, Uri conditionId, String caller, String reason) {

       setManualZenMode(zenMode, conditionId, reason, caller, true /*setRingerMode*/);

       Settings.Secure.putInt(mContext.getContentResolver(),

               Settings.Secure.SHOW_ZEN_SETTINGS_SUGGESTION, 0);

   }

   private void setManualZenMode(int zenMode, Uri conditionId, String reason, String caller,

           boolean setRingerMode) {

       ZenModeConfig newConfig;

       synchronized (mConfig) {

           if (mConfig == null) return;

           if (!Global.isValidZenMode(zenMode)) return;

           if (DEBUG) Log.d(TAG, "setManualZenMode " + Global.zenModeToString(zenMode)

                   + " conditionId=" + conditionId + " reason=" + reason

                   + " setRingerMode=" + setRingerMode);

           newConfig = mConfig.copy();

           if (zenMode == Global.ZEN_MODE_OFF) {

               newConfig.manualRule = null;

               for (ZenRule automaticRule : newConfig.automaticRules.values()) {

                   if (automaticRule.isAutomaticActive()) {

                       automaticRule.snoozing = true;

                   }

               }

           } else {

               final ZenRule newRule = new ZenRule();

               newRule.enabled = true;

               newRule.zenMode = zenMode;

               newRule.conditionId = conditionId;

               newRule.enabler = caller;

               newConfig.manualRule = newRule;

           }

           setConfigLocked(newConfig, reason, null, setRingerMode);

       }

   }

   private boolean setConfigLocked(ZenModeConfig config, String reason,

           ComponentName triggeringComponent, boolean setRingerMode) {

       final long identity = Binder.clearCallingIdentity();

       try {

           if (config == null || !config.isValid()) {

               Log.w(TAG, "Invalid config in setConfigLocked; " + config);

               return false;

           }

           if (config.user != mUser) {

               // simply store away for background users

               mConfigs.put(config.user, config);

               if (DEBUG) Log.d(TAG, "setConfigLocked: store config for user " + config.user);

               return true;

           }

           // handle CPS backed conditions - danger! may modify config

           mConditions.evaluateConfig(config, null, false /*processSubscriptions*/);

           mConfigs.put(config.user, config);

           if (DEBUG) Log.d(TAG, "setConfigLocked reason=" + reason, new Throwable());

           ZenLog.traceConfig(reason, mConfig, config);

           // send some broadcasts

           final boolean policyChanged = !Objects.equals(getNotificationPolicy(mConfig),

                   getNotificationPolicy(config));

           if (!config.equals(mConfig)) {

               mConfig = config;

               dispatchOnConfigChanged();

               updateConsolidatedPolicy(reason);

           }

           if (policyChanged) {

               dispatchOnPolicyChanged();

           }

           mHandler.postApplyConfig(config, reason, triggeringComponent, setRingerMode);

           return true;

       } catch (SecurityException e) {

           Log.wtf(TAG, "Invalid rule in config", e);

           return false;

       } finally {

           Binder.restoreCallingIdentity(identity);

       }

   }

   private final class H extends Handler {

       private void postApplyConfig(ZenModeConfig config, String reason,

               ComponentName triggeringComponent, boolean setRingerMode) {

           sendMessage(obtainMessage(MSG_APPLY_CONFIG,

                   new ConfigMessageData(config, reason, triggeringComponent, setRingerMode)));

       }

       @Override

       public void handleMessage(Message msg) {

           switch (msg.what) {

               case MSG_DISPATCH:

                   dispatchOnZenModeChanged();

                   break;

               case MSG_METRICS:

                   mMetrics.emit();

                   break;

               case MSG_APPLY_CONFIG:

                   ConfigMessageData applyConfigData = (ConfigMessageData) msg.obj;

                   applyConfig(applyConfigData.config, applyConfigData.reason,

                           applyConfigData.triggeringComponent, applyConfigData.setRingerMode);

           }

       }

   }

   private void applyConfig(ZenModeConfig config, String reason,

           ComponentName triggeringComponent, boolean setRingerMode) {

       final String val = Integer.toString(config.hashCode());

       Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val);

       // 应用勿扰模式的配置

       evaluateZenMode(reason, setRingerMode);

       // 设置勿扰模式的时间段

       mConditions.evaluateConfig(config, triggeringComponent, true /*processSubscriptions*/);

   }

   @VisibleForTesting

   protected void evaluateZenMode(String reason, boolean setRingerMode) {

       if (DEBUG) Log.d(TAG, "evaluateZenMode");

       if (mConfig == null) return;

       final int policyHashBefore = mConsolidatedPolicy == null ? 0

               : mConsolidatedPolicy.hashCode();

       final int zenBefore = mZenMode;

       // 通过计算活得勿扰模式的状态

       final int zen = computeZenMode();

       ZenLog.traceSetZenMode(zen, reason);

       mZenMode = zen;

       setZenModeSetting(mZenMode);//根据勿扰模式的状态设置Settings数据库值

       updateConsolidatedPolicy(reason);//更新勿扰模式统一的策略

       updateRingerModeAffectedStreams();//更新铃声模式受影响的铃声流

       if (setRingerMode && (zen != zenBefore || (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS

               && policyHashBefore != mConsolidatedPolicy.hashCode()))) {

           applyZenToRingerMode();//更新铃声模式

       }

       applyRestrictions();//引用勿扰模式的各项限制

       if (zen != zenBefore) {

           mHandler.postDispatchOnZenModeChanged();//回调方法通知勿扰模式发生改变

       }

   }

}

总结:

通过快捷方式设置开启“勿扰模式”,首先进入到DndTile类,handleClick方法是对该Tile的点击事件做处理的,无论

是开启或关闭“勿扰模式”最终都会调用ZenModeControllerImpl#setZen进行设置,只是传入的参数一样

通过AIDL的方式调用NotificationManagerService#setZenMode方法设置勿扰模式

调用ZenModeHelper.setManualZenMode#setManualZenMode方法进行设置勿扰模式和更新规则

setConfigLocked(newConfig, "setNotificationPolicy")方法中先对配置文件进行空判断和有效判断。然后判断配置

文件相对于原始是否有变得,如果有,调用dispatchOnConfigChanged(),该方法内部是一个回调,会调用到

NotificationManagerService#onConfigChanged()

找到onConfigChanged(),调用handleSavePolicyFile()发送消息执行SavePolicyFileRunnable的run方法,该方法内

部调用writePolicyXml(),该方法通过ZenModeHelper对象再继续调用writeXml(...),就这样

系统将之前解析出来

的数据信息,也就是用户设定的各种勿扰的配置信息写在xml里面

各个apk对勿扰模式的处理,以来电铃声为例

public class Ringer {

       private boolean shouldRingForContact(Uri contactUri) {

       final NotificationManager manager =

               (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);

       final Bundle peopleExtras = new Bundle();

       if (contactUri != null) {

           ArrayList personList = new ArrayList<>();

           personList.add(new Person.Builder().setUri(contactUri.toString()).build());

           peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList);

       }

       return manager.matchesCallFilter(peopleExtras);

   }

}

调用matchesCallFilter方法来决定是否响铃。

public class NotificationManager {

    public boolean matchesCallFilter(Bundle extras) {

        INotificationManager service = getService();

        try {

            return service.matchesCallFilter(extras);

        } catch (RemoteException e) {

            throw e.rethrowFromSystemServer();

        }

    }

}

public class NotificationManagerService extends SystemService {

    final IBinder mService = new INotificationManager.Stub() {

        @Override

        public boolean matchesCallFilter(Bundle extras) {

            // Because matchesCallFilter may use contact data to filter calls, the callers of this

            // method need to either have notification listener access or permission to read

            // contacts.

            boolean systemAccess = false;

            try {

                enforceSystemOrSystemUI("INotificationManager.matchesCallFilter");

                systemAccess = true;

            } catch (SecurityException e) {

            }

            boolean listenerAccess = false;

            try {

                String[] pkgNames = mPackageManager.getPackagesForUid(Binder.getCallingUid());

                for (int i = 0; i < pkgNames.length; i++) {

                    // in most cases there should only be one package here

                    listenerAccess |= mListeners.hasAllowedListener(pkgNames[i],

                            Binder.getCallingUserHandle().getIdentifier());

                }

            } catch (RemoteException e) {

            } finally {

                if (!systemAccess && !listenerAccess) {

                    getContext().enforceCallingPermission(Manifest.permission.READ_CONTACTS,

                            "matchesCallFilter requires listener permission, contacts read access,"

                            + " or system level access");

                }

            }

            return mZenModeHelper.matchesCallFilter(

                    Binder.getCallingUserHandle(),

                    extras,

                    mRankingHelper.findExtractor(ValidateNotificationPeople.class),

                    MATCHES_CALL_FILTER_CONTACTS_TIMEOUT_MS,

                    MATCHES_CALL_FILTER_TIMEOUT_AFFINITY);

        }

       }

}

public class ZenModeHelper {

   public boolean matchesCallFilter(UserHandle userHandle, Bundle extras,

           ValidateNotificationPeople validator, int contactsTimeoutMs, float timeoutAffinity) {

       synchronized (mConfig) {

           return ZenModeFiltering.matchesCallFilter(mContext, mZenMode, mConsolidatedPolicy,

                   userHandle, extras, validator, contactsTimeoutMs, timeoutAffinity);

       }

       }

}

public class ZenModeFiltering {

    public static boolean matchesCallFilter(Context context, int zen, NotificationManager.Policy

            consolidatedPolicy, UserHandle userHandle, Bundle extras,

            ValidateNotificationPeople validator, int contactsTimeoutMs, float timeoutAffinity) {

        if (zen == Global.ZEN_MODE_NO_INTERRUPTIONS) {

            ZenLog.traceMatchesCallFilter(false, "no interruptions");

            return false; // nothing gets through

        } // 勿扰模式判断

        if (zen == Global.ZEN_MODE_ALARMS) {

            ZenLog.traceMatchesCallFilter(false, "alarms only");

            return false; // not an alarm

        }

        if (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) {

            if (consolidatedPolicy.allowRepeatCallers()

                    && REPEAT_CALLERS.isRepeat(context, extras, null)) {

                ZenLog.traceMatchesCallFilter(true, "repeat caller");

                return true;

            } // 短时间内第二次来电放行

            if (!consolidatedPolicy.allowCalls()) {

                ZenLog.traceMatchesCallFilter(false, "calls not allowed");

                return false; // no other calls get through

            } // 是否禁止全部来电

            if (validator != null) {

                final float contactAffinity = validator.getContactAffinity(userHandle, extras,

                        contactsTimeoutMs, timeoutAffinity);

                boolean match =

                        audienceMatches(consolidatedPolicy.allowCallsFrom(), contactAffinity);

                ZenLog.traceMatchesCallFilter(match, "contact affinity " + contactAffinity);

                return match;

            } // 根据联系人判断

        }

        ZenLog.traceMatchesCallFilter(true, "no restrictions");

        return true;

    }

    private static boolean audienceMatches(int source, float contactAffinity) {

        switch (source) {

            case ZenModeConfig.SOURCE_ANYONE:

                return true; // 所有人

            case ZenModeConfig.SOURCE_CONTACT:

                return contactAffinity >= ValidateNotificationPeople.VALID_CONTACT; // 联系人

            case ZenModeConfig.SOURCE_STAR:

                return contactAffinity >= ValidateNotificationPeople.STARRED_CONTACT; // 星标联系人

            default:

                Slog.w(TAG, "Encountered unknown source: " + source);

                return true;

        }

    }

}

就是依据配置走不同的分支。接下来继续看核心的getContactAffinity方法、线程工作方法work如

下:

public class ValidateNotificationPeople implements NotificationSignalExtractor {

   public float getContactAffinity(UserHandle userHandle, Bundle extras, int timeoutMs,

           float timeoutAffinity) {

       if (DEBUG) Slog.d(TAG, "checking affinity for " + userHandle);

       if (extras == null) return NONE;

       final String key = Long.toString(System.nanoTime());

       final float[] affinityOut = new float[1];

       Context context = getContextAsUser(userHandle);

       if (context == null) {

           return NONE;

       }

       final PeopleRankingReconsideration prr =

               validatePeople(context, key, extras, null, affinityOut);

       float affinity = affinityOut[0];

       if (prr != null) {

           // Perform the heavy work on a background thread so we can abort when we hit the

           // timeout.

           final Semaphore s = new Semaphore(0);

           AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {

               @Override

               public void run() {

                   prr.work(); // 开线程查数据库

                   s.release();

               }

           });

           try {

               if (!s.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS)) {

                   Slog.w(TAG, "Timeout while waiting for affinity: " + key + ". "

                           + "Returning timeoutAffinity=" + timeoutAffinity);

                   return timeoutAffinity; // 超时后直接返回

               }

           } catch (InterruptedException e) {

               Slog.w(TAG, "InterruptedException while waiting for affinity: " + key + ". "

                       + "Returning affinity=" + affinity, e);

               return affinity; // 线程被中断后返回缓存结果

           }

           affinity = Math.max(prr.getContactAffinity(), affinity); //正常情况下返回的结果

       }

       return affinity;

   }

   private class PeopleRankingReconsideration extends RankingReconsideration {

       @Override

       public void work() {

           if (VERBOSE) Slog.i(TAG, "Executing: validation for: " + mKey);

           long timeStartMs = System.currentTimeMillis();

           for (final String handle: mPendingLookups) {

               final String cacheKey = getCacheKey(mContext.getUserId(), handle);

               LookupResult lookupResult = null;

               boolean cacheHit = false;

               synchronized (mPeopleCache) {

                   lookupResult = mPeopleCache.get(cacheKey);

                   if (lookupResult != null && !lookupResult.isExpired()) {

                       // The name wasn't already added to the cache, no need to retry

                       cacheHit = true;

                   }

               }

               if (!cacheHit) {

                   final Uri uri = Uri.parse(handle);

                   if ("tel".equals(uri.getScheme())) { // 处理电话号码类型的uri

                       if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle);

                       lookupResult = resolvePhoneContact(mContext, uri.getSchemeSpecificPart());

                   } else if ("mailto".equals(uri.getScheme())) { // 处理电子邮件类型的uri

                       if (DEBUG) Slog.d(TAG, "checking mailto URI: " + handle);

                       lookupResult = resolveEmailContact(mContext, uri.getSchemeSpecificPart());

                   } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { // 处理联系人lookup_uri

                       if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle);

                       // only look up phone number if this is a contact lookup uri and thus isn't

                       // already directly a phone number.

                       lookupResult = searchContactsAndLookupNumbers(mContext, uri);

                   } else { // 非法的uri,生成默认的结果

                       lookupResult = new LookupResult();  // invalid person for the cache

                       if (!"name".equals(uri.getScheme())) {

                           Slog.w(TAG, "unsupported URI " + handle);

                       }

                   }

               }

               if (lookupResult != null) {

                   if (!cacheHit) {

                       synchronized (mPeopleCache) {

                           mPeopleCache.put(cacheKey, lookupResult); // 查询结果存储到缓存中

                       }

                   }

                   if (DEBUG) {

                       Slog.d(TAG, "lookup contactAffinity is " + lookupResult.getAffinity());

                   }

                   mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity()); // 保存结果值

                   // merge any phone numbers found in this lookup result

                   if (lookupResult.getPhoneNumbers() != null) {

                       if (mPhoneNumbers == null) {

                           mPhoneNumbers = new ArraySet<>();

                       }

                       mPhoneNumbers.addAll(lookupResult.getPhoneNumbers());

                   }

               } else {

                   if (DEBUG) Slog.d(TAG, "lookupResult is null");

               }

           }

           if (DEBUG) {

               Slog.d(TAG, "Validation finished in " + (System.currentTimeMillis() - timeStartMs) +

                       "ms");

           }

           if (mRecord != null) {

               mUsageStats.registerPeopleAffinity(mRecord, mContactAffinity > NONE,

                       mContactAffinity == STARRED_CONTACT, false /* cached */);

           }

       }

   }

}

public class ValidateNotificationPeople implements NotificationSignalExtractor {

   private LookupResult resolvePhoneContact(Context context, final String number) {

       Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,

               Uri.encode(number));

       return searchContacts(context, phoneUri);

   }

   LookupResult searchContacts(Context context, Uri lookupUri) {

       LookupResult lookupResult = new LookupResult();

       final Uri corpLookupUri =

               ContactsContract.Contacts.createCorpLookupUriFromEnterpriseLookupUri(lookupUri);

       if (corpLookupUri == null) {

           addContacts(lookupResult, context, lookupUri);

       } else {

           addWorkContacts(lookupResult, context, corpLookupUri);

       }

       return lookupResult;

   }

   LookupResult searchContacts(Context context, Uri lookupUri) {

       LookupResult lookupResult = new LookupResult();

       final Uri corpLookupUri =

               ContactsContract.Contacts.createCorpLookupUriFromEnterpriseLookupUri(lookupUri);

       if (corpLookupUri == null) {

           addContacts(lookupResult, context, lookupUri);

       } else {

           addWorkContacts(lookupResult, context, corpLookupUri);

       }

       return lookupResult;

   }

   private void addContacts(LookupResult lookupResult, Context context, Uri uri) {

       try (Cursor c = context.getContentResolver().query(

               uri, LOOKUP_PROJECTION, null, null, null)) {

           if (c == null) {

               Slog.w(TAG, "Null cursor from contacts query.");

               return;

           }

           while (c.moveToNext()) {

               lookupResult.mergeContact(c);

           }

       } catch (Throwable t) {

           Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);

       }

   }

}

上述几个方法就是查询联系人数据库,其中生成结果的方法mergeContact如下:

private static class LookupResult {

   private float mAffinity = NONE;

   public void mergeContact(Cursor cursor) {

       mAffinity = Math.max(mAffinity, VALID_CONTACT);

       // Contact ID

       int id;

       final int idIdx = cursor.getColumnIndex(Contacts._ID);

       if (idIdx >= 0) {

           id = cursor.getInt(idIdx);

           if (DEBUG) Slog.d(TAG, "contact _ID is: " + id);

       } else {

           id = -1;

           Slog.i(TAG, "invalid cursor: no _ID");

       }

       // Lookup key for potentially looking up contact phone number later

       final int lookupKeyIdx = cursor.getColumnIndex(Contacts.LOOKUP_KEY);

       if (lookupKeyIdx >= 0) {

           mPhoneLookupKey = cursor.getString(lookupKeyIdx);

           if (DEBUG) Slog.d(TAG, "contact LOOKUP_KEY is: " + mPhoneLookupKey);

       } else {

           if (DEBUG) Slog.d(TAG, "invalid cursor: no LOOKUP_KEY");

       }

       // Starred

       final int starIdx = cursor.getColumnIndex(Contacts.STARRED);

       if (starIdx >= 0) {

           boolean isStarred = cursor.getInt(starIdx) != 0;

           if (isStarred) {

               mAffinity = Math.max(mAffinity, STARRED_CONTACT);

           }

           if (DEBUG) Slog.d(TAG, "contact STARRED is: " + isStarred);

       } else {

           if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED");

       }

       // whether a phone number is present

       final int hasPhoneIdx = cursor.getColumnIndex(Contacts.HAS_PHONE_NUMBER);

       if (hasPhoneIdx >= 0) {

           mHasPhone = cursor.getInt(hasPhoneIdx) != 0;

           if (DEBUG) Slog.d(TAG, "contact HAS_PHONE_NUMBER is: " + mHasPhone);

       } else {

           if (DEBUG) Slog.d(TAG, "invalid cursor: no HAS_PHONE_NUMBER");

       }

   }

}

实际上就是给mAffinity赋值,标记为普通联系人或者星标联系人。

相关文章
|
6月前
|
Android开发
Android Mediatek 增加Recovery模式下读cmdline的强制工厂重置选项
Android Mediatek 增加Recovery模式下读cmdline的强制工厂重置选项
59 0
|
6月前
|
XML 前端开发 测试技术
Android基础知识:解释Android的MVC和MVP模式。
Android基础知识:解释Android的MVC和MVP模式。
66 0
|
测试技术 Android开发 虚拟化
踩坑记录 | Android 逆向之如何处理 Kali Nat 模式无法上网?
踩坑记录 | Android 逆向之如何处理 Kali Nat 模式无法上网?
307 0
|
3月前
|
设计模式 Android开发 Kotlin
Android经典实战之Kotlin委托模式和by关键字
本文介绍了Kotlin中`by`关键字在类及属性委托中的运用,通过实例展示了如何利用类委托简化接口实现,以及如何借助标准与自定义属性委托管理属性的读写操作。通过`by`关键字的支持,Kotlin使得委托模式的实现更为直观且高效。
81 4
|
4月前
|
存储 前端开发 测试技术
Android Kotlin中使用 LiveData、ViewModel快速实现MVVM模式
使用Kotlin实现MVVM模式是Android开发的现代实践。该模式分离UI和业务逻辑,借助LiveData、ViewModel和DataBinding增强代码可维护性。步骤包括创建Model层处理数据,ViewModel层作为数据桥梁,以及View层展示UI。添加相关依赖后,Model类存储数据,ViewModel类通过LiveData管理变化,而View层使用DataBinding实时更新UI。这种架构提升代码可测试性和模块化。
186 2
|
6月前
|
传感器 小程序 Java
Java+saas模式 智慧校园系统源码Java Android +MySQL+ IDEA 多校运营数字化校园云平台源码
Java+saas模式 智慧校园系统源码Java Android +MySQL+ IDEA 多校运营数字化校园云平台源码 智慧校园即智慧化的校园,也指按智慧化标准进行的校园建设,按标准《智慧校园总体框架》中对智慧校园的标准定义是:物理空间和信息空间的有机衔接,使任何人、任何时间、任何地点都能便捷的获取资源和服务。
68 1
|
6月前
|
设计模式 前端开发 数据库
构建高效Android应用:使用Jetpack架构组件实现MVVM模式
【4月更文挑战第21天】 在移动开发领域,构建一个既健壮又易于维护的Android应用是每个开发者的目标。随着项目复杂度的增加,传统的MVP或MVC架构往往难以应对快速变化的市场需求和复杂的业务逻辑。本文将探讨如何利用Android Jetpack中的架构组件来实施MVVM(Model-View-ViewModel)设计模式,旨在提供一个更加模块化、可测试且易于管理的代码结构。通过具体案例分析,我们将展示如何使用LiveData, ViewModel, 和Repository来实现界面与业务逻辑的分离,以及如何利用Room数据库进行持久化存储。最终,你将获得一个响应迅速、可扩展且符合现代软件工
92 0
|
6月前
|
XML 数据库 数据安全/隐私保护
Android App规范处理中版本设置、发布模式、给数据集SQLite加密的讲解及使用(附源码 超详细必看)
Android App规范处理中版本设置、发布模式、给数据集SQLite加密的讲解及使用(附源码 超详细必看)
76 0
|
Android开发
Android 应用程序一直处于竖屏模式(又称肖像模式)
Android 应用程序一直处于竖屏模式(又称肖像模式)
175 0
|
4天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。