Lawnchair: Port Lawnfeed/Bridge to A11

This commit is contained in:
Kshitij Gupta
2021-03-19 03:38:39 +05:30
parent 8453943026
commit bfce6ad32b
21 changed files with 456 additions and 34 deletions
+1 -1
View File
@@ -43,7 +43,7 @@
attributes and intent filters the same
-->
<activity
android:name="com.android.launcher3.Launcher"
android:name="ch.deletescape.lawnchair.LawnchairLauncher"
android:launchMode="singleTask"
android:clearTaskOnLaunch="true"
android:stateNotNeeded="true"
+1
View File
@@ -189,6 +189,7 @@ android {
lawn {
java.srcDirs = ['src_flags', 'src_shortcuts_overrides', 'lawnchair/src']
aidl.srcDirs = ['lawnchair/aidl']
res.srcDirs = ['lawnchair/res']
manifest.srcFile "lawnchair/AndroidManifest.xml"
}
@@ -0,0 +1,42 @@
package com.google.android.libraries.launcherclient;
import android.view.WindowManager.LayoutParams;
import com.google.android.libraries.launcherclient.ILauncherOverlayCallback;
interface ILauncherOverlay {
oneway void startScroll();
oneway void onScroll(in float progress);
oneway void endScroll();
oneway void windowAttached(in LayoutParams lp, in ILauncherOverlayCallback cb, in int flags);
oneway void windowDetached(in boolean isChangingConfigurations);
oneway void closeOverlay(in int flags);
oneway void onPause();
oneway void onResume();
oneway void openOverlay(in int flags);
oneway void requestVoiceDetection(in boolean start);
String getVoiceSearchLanguage();
boolean isVoiceDetectionRunning();
boolean hasOverlayContent();
oneway void windowAttached2(in Bundle bundle, in ILauncherOverlayCallback cb);
oneway void unusedMethod();
oneway void setActivityState(in int flags);
boolean startSearch(in byte[] data, in Bundle bundle);
}
@@ -0,0 +1,9 @@
package com.google.android.libraries.launcherclient;
interface ILauncherOverlayCallback {
oneway void overlayScrollChanged(float progress);
oneway void overlayStatusChanged(int status);
}
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="bridge_signature_hash">0xB662CC2F</integer>
<string name="bridge_version" translatable="false">v3</string>
<integer name="lawnfeed_signature_hash">0x7F7A957A</integer>
</resources>
@@ -19,37 +19,57 @@ package ch.deletescape.lawnchair
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo.FLAG_SYSTEM
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Process
import android.util.Log
import ch.deletescape.lawnchair.util.SingletonHolder
import ch.deletescape.lawnchair.util.preferences.LawnchairPreferences
import com.android.launcher3.BuildConfig
import com.android.launcher3.R
import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.Utilities
class FeedBridge(private val context: Context) {
private val bridgePackages = listOf(
PixelBridgeInfo("com.google.android.apps.nexuslauncher", R.integer.bridge_signature_hash),
BridgeInfo("ch.deletescape.lawnchair.lawnfeed", R.integer.lawnfeed_signature_hash))
private val shouldUseFeed = context.applicationInfo.flags and FLAG_SYSTEM == 0
private val prefs = LawnchairPreferences.getInstance(context)
private val bridgePackages by lazy {
listOf(
PixelBridgeInfo("com.google.android.apps.nexuslauncher", R.integer.bridge_signature_hash),
BridgeInfo("ch.deletescape.lawnchair.lawnfeed", R.integer.lawnfeed_signature_hash))
}
fun resolveBridge(): BridgeInfo? {
if (!useBridge) return null
val customBridge = customBridgeOrNull()
if (customBridge != null) {
return customBridge
}
if (!shouldUseFeed) return null
return bridgePackages.firstOrNull { it.isAvailable() }
}
private fun customBridgeOrNull() = if (prefs!!.getString(LawnchairPreferences.FEED_PROVIDER, "")!!.isNotBlank()) {
val bridge = prefs.getString(LawnchairPreferences.FEED_PROVIDER, "")?.let { CustomBridgeInfo(it) }
if (bridge!!.isAvailable()) {
bridge
} else null
} else null
private fun customBridgeAvailable() = customBridgeOrNull()?.isAvailable() == true
fun isInstalled(): Boolean {
return !useBridge || bridgePackages.any { it.isAvailable() }
return customBridgeAvailable() || !shouldUseFeed || bridgePackages.any { it.isAvailable() }
}
fun resolveSmartspace(): String {
return bridgePackages.firstOrNull { it.supportsSmartspace }?.packageName ?: "com.google.android.googlequicksearchbox"
return bridgePackages.firstOrNull { it.supportsSmartspace }?.packageName
?: "com.google.android.googlequicksearchbox"
}
open inner class BridgeInfo(val packageName: String, signatureHashRes: Int) {
private val signatureHash = context.resources.getInteger(signatureHashRes)
protected open val signatureHash = if (signatureHashRes > 0) context.resources.getInteger(signatureHashRes) else 0
open val supportsSmartspace = false
@@ -63,16 +83,16 @@ class FeedBridge(private val context: Context) {
.append(Process.myUid())
.toString())
.buildUpon()
.appendQueryParameter("v", Integer.toString(7))
.appendQueryParameter("cv", Integer.toString(9))
.appendQueryParameter("v", 7.toString())
.appendQueryParameter("cv", 9.toString())
.build()), 0)
return info != null && isSigned()
}
private fun isSigned(): Boolean {
if (BuildConfig.FLAVOR_build.equals("dev"))
open fun isSigned(): Boolean {
if (BuildConfig.DEBUG)
return true // Skip signature checks for dev builds
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (Utilities.ATLEAST_P) {
val info = context.packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES)
val signingInfo = info.signingInfo
if (signingInfo.hasMultipleSigners()) return false
@@ -87,18 +107,48 @@ class FeedBridge(private val context: Context) {
}
}
private inner class CustomBridgeInfo(packageName: String) : BridgeInfo(packageName, 0) {
override val signatureHash = whitelist[packageName]?.toInt() ?: -1
private val disableWhitelist = prefs!!.getBoolean(LawnchairPreferences.IGNORE_FEED_WHITELIST, false)
override fun isSigned(): Boolean {
if (signatureHash == -1 && Utilities.ATLEAST_P) {
val info = context.packageManager
.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES)
val signingInfo = info.signingInfo
if (signingInfo.hasMultipleSigners()) return false
signingInfo.signingCertificateHistory.forEach {
val hash = Integer.toHexString(it.hashCode())
Log.d(TAG, "Feed provider $packageName(0x$hash) isn't whitelisted")
}
}
return disableWhitelist || signatureHash != -1 && super.isSigned()
}
}
private inner class PixelBridgeInfo(packageName: String, signatureHashRes: Int) : BridgeInfo(packageName, signatureHashRes) {
override val supportsSmartspace get() = isAvailable()
}
companion object : SingletonHolder<FeedBridge, Context>(ensureOnMainThread(
useApplicationContext(::FeedBridge))) {
private const val TAG = "FeedBridge"
private const val overlayAction = "com.android.launcher3.WINDOW_OVERLAY"
private val whitelist = mapOf<String, Long>(
"ua.itaysonlab.homefeeder" to 0x887456ed, // HomeFeeder, t.me/homefeeder
"launcher.libre.dev" to 0x2e9dbab5 // Librechair, t.me/librechair
)
fun getAvailableProviders(context: Context) = context.packageManager
.queryIntentServices(Intent(overlayAction).setData(Uri.parse("app://" + context.packageName)), PackageManager.GET_META_DATA)
.map { it.serviceInfo.applicationInfo }
.distinct()
.filter { getInstance(context).CustomBridgeInfo(it.packageName).isSigned() }
@JvmStatic
val useBridge = FeatureFlags.FORCE_FEED_BRIDGE || !BuildConfig.DEBUG
fun useBridge(context: Context) = getInstance(context).let { it.shouldUseFeed || it.customBridgeAvailable() }
}
}
@@ -0,0 +1,13 @@
package ch.deletescape.lawnchair;
import com.android.launcher3.Launcher;
import com.android.systemui.plugins.shared.LauncherOverlayManager;
import ch.deletescape.nexuslauncher.OverlayCallbackImpl;
public class LawnchairLauncher extends Launcher {
@Override
protected LauncherOverlayManager getDefaultOverlay() {
return new OverlayCallbackImpl(this);
}
}
@@ -0,0 +1,13 @@
package ch.deletescape.lawnchair;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.systemui.plugins.shared.LauncherOverlayManager;
import ch.deletescape.nexuslauncher.OverlayCallbackImpl;
public class LawnchairLauncherQuickstep extends QuickstepLauncher {
@Override
protected LauncherOverlayManager getDefaultOverlay() {
return new OverlayCallbackImpl(this);
}
}
@@ -0,0 +1,46 @@
/*
* This file is part of Lawnchair Launcher.
*
* Lawnchair Launcher is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Lawnchair Launcher is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Lawnchair Launcher. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.deletescape.lawnchair
import android.content.Context
import android.os.Looper
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import java.util.concurrent.Callable
import java.util.concurrent.ExecutionException
fun <T, A>ensureOnMainThread(creator: (A) -> T): (A) -> T {
return { it ->
if (Looper.myLooper() == Looper.getMainLooper()) {
creator(it)
} else {
try {
MAIN_EXECUTOR.submit(Callable { creator(it) }).get()
} catch (e: InterruptedException) {
throw RuntimeException(e)
} catch (e: ExecutionException) {
throw RuntimeException(e)
}
}
}
}
fun <T>useApplicationContext(creator: (Context) -> T): (Context) -> T {
return { it -> creator(it.applicationContext) }
}
@@ -0,0 +1,53 @@
/*
* This file is part of Lawnchair Launcher.
*
* Lawnchair Launcher is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Lawnchair Launcher is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Lawnchair Launcher. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.deletescape.lawnchair.util
import android.content.Context
import ch.deletescape.lawnchair.ensureOnMainThread
import ch.deletescape.lawnchair.useApplicationContext
// Source: https://medium.com/@BladeCoder/kotlin-singletons-with-argument-194ef06edd9e
open class SingletonHolder<out T, in A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@Volatile
private var instance: T? = null
open fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
fun dangerousGetInstance() = instance
}
open class LawnchairSingletonHolder<out T>(creator: (Context) -> T)
: SingletonHolder<T, Context>(ensureOnMainThread(useApplicationContext(creator)))
@@ -74,6 +74,15 @@ class LawnchairPreferences(val context: Context) {
@kotlin.jvm.JvmField
var MAKE_COLORED_BACKGROUNDS: String = "pref_makeColoredBackgrounds"
@kotlin.jvm.JvmField
var IGNORE_FEED_WHITELIST: String = "pref_ignoreFeedWhitelist"
@kotlin.jvm.JvmField
var FEED_PROVIDER: String = "pref_feedProvider"
@kotlin.jvm.JvmField
var ENABLE_MINUS_ONE: String = "pref_enableMinusOne"
fun getInstance(context: Context?): SharedPreferences? = when {
context == null -> null
INSTANCE == null -> Utilities.getPrefs(context)
@@ -0,0 +1,179 @@
package ch.deletescape.nexuslauncher;
import static ch.deletescape.lawnchair.util.preferences.LawnchairPreferences.ENABLE_MINUS_ONE;
import android.app.Activity;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import com.android.launcher3.Launcher;
import com.android.launcher3.Utilities;
import com.android.systemui.plugins.shared.LauncherOverlayManager;
import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
import com.google.android.libraries.launcherclient.ISerializableScrollCallback;
import com.google.android.libraries.launcherclient.LauncherClient;
import com.google.android.libraries.launcherclient.LauncherClientCallbacks;
import com.google.android.libraries.launcherclient.StaticInteger;
import ch.deletescape.lawnchair.FeedBridge;
import ch.deletescape.lawnchair.util.preferences.LawnchairPreferences;
/**
* Implements {@link LauncherOverlay} and passes all the corresponding events to {@link
* LauncherClient}. {@see setClient}
*
* <p>Implements {@link LauncherClientCallbacks} and sends all the corresponding callbacks to {@link
* Launcher}.
*/
public class OverlayCallbackImpl
implements LauncherOverlay, LauncherClientCallbacks, LauncherOverlayManager,
OnSharedPreferenceChangeListener, ISerializableScrollCallback {
final static String PREF_PERSIST_FLAGS = "pref_persistent_flags";
private final Launcher mLauncher;
private final LauncherClient mClient;
private LauncherOverlayCallbacks mLauncherOverlayCallbacks;
private boolean mWasOverlayAttached = false;
boolean mFlagsChanged = false;
private int mFlags;
public OverlayCallbackImpl(Launcher launcher) {
SharedPreferences prefs = Utilities.getPrefs(launcher);
mLauncher = launcher;
mClient = new LauncherClient(mLauncher, this, new StaticInteger(
(prefs.getBoolean(LawnchairPreferences.ENABLE_MINUS_ONE,
FeedBridge.useBridge(launcher)) ? 1 : 0) | 2 | 4 | 8));
prefs.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onDeviceProvideChanged() {
mClient.redraw();
}
@Override
public void onAttachedToWindow() {
mClient.onAttachedToWindow();
}
@Override
public void onDetachedFromWindow() {
mClient.onDetachedFromWindow();
}
@Override
public void openOverlay() {
mClient.showOverlay(true);
}
@Override
public void hideOverlay(boolean animate) {
mClient.hideOverlay(animate);
}
@Override
public void hideOverlay(int duration) {
mClient.hideOverlay(duration);
}
@Override
public boolean startSearch(byte[] config, Bundle extras) {
return false;
}
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
// Not called
}
@Override
public void onActivityStarted(Activity activity) {
mClient.onStart();
}
@Override
public void onActivityResumed(Activity activity) {
mClient.onResume();
}
@Override
public void onActivityPaused(Activity activity) {
mClient.onPause();
}
@Override
public void onActivityStopped(Activity activity) {
mClient.onStop();
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { }
@Override
public void onActivityDestroyed(Activity activity) {
mClient.mDestroyed = true;
mLauncher.getSharedPrefs().unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
if (ENABLE_MINUS_ONE.equals(key)) {
mClient.showOverlay(prefs.getBoolean(ENABLE_MINUS_ONE, FeedBridge.useBridge(mLauncher)));
}
}
@Override
public void onOverlayScrollChanged(float progress) {
if (mLauncherOverlayCallbacks != null) {
mLauncherOverlayCallbacks.onScrollChanged(progress);
}
}
@Override
public void onServiceStateChanged(boolean overlayAttached, boolean hotwordActive) {
this.onServiceStateChanged(overlayAttached);
}
@Override
public void onServiceStateChanged(boolean overlayAttached) {
if (overlayAttached != mWasOverlayAttached) {
mWasOverlayAttached = overlayAttached;
mLauncher.setLauncherOverlay(overlayAttached ? this : null);
}
}
@Override
public void onScrollInteractionBegin() {
mClient.startScroll();
}
@Override
public void onScrollInteractionEnd() {
mClient.endScroll();
}
@Override
public void onScrollChange(float progress, boolean rtl) {
mClient.setScroll(progress);
}
@Override
public void setOverlayCallbacks(LauncherOverlayCallbacks callbacks) {
mLauncherOverlayCallbacks = callbacks;
}
@Override
public void setPersistentFlags(int flags) {
flags &= (8 | 16);
if (flags != mFlags) {
mFlagsChanged = true;
mFlags = flags;
Utilities.getDevicePrefs(mLauncher).edit().putInt(PREF_PERSIST_FLAGS, flags).apply();
}
}
}
@@ -1,4 +1,4 @@
package com.google.android.libraries.gsa.launcherclient;
package com.google.android.libraries.launcherclient;
import android.content.ComponentName;
import android.content.Context;
@@ -1,4 +1,4 @@
package com.google.android.libraries.gsa.launcherclient;
package com.google.android.libraries.launcherclient;
public interface IScrollCallback {
void onOverlayScrollChanged(float progress);
@@ -1,4 +1,4 @@
package com.google.android.libraries.gsa.launcherclient;
package com.google.android.libraries.launcherclient;
public interface ISerializableScrollCallback extends IScrollCallback {
void setPersistentFlags(int flags);
@@ -1,4 +1,4 @@
package com.google.android.libraries.gsa.launcherclient;
package com.google.android.libraries.launcherclient;
import android.app.Activity;
import android.content.BroadcastReceiver;
@@ -22,15 +22,13 @@ import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import ch.deletescape.lawnchair.FeedBridge;
import ch.deletescape.lawnchair.FeedBridge.BridgeInfo;
import com.google.android.libraries.launcherclient.ILauncherOverlay;
import com.google.android.libraries.launcherclient.ILauncherOverlayCallback;
import java.lang.ref.WeakReference;
public class LauncherClient {
private static int apiVersion = -1;
private ILauncherOverlay mOverlay;
private final IScrollCallback mScrollCallback;
public final IScrollCallback mScrollCallback;
public final BaseClientService mBaseService;
public final LauncherClientService mLauncherService;
@@ -188,7 +186,9 @@ public class LauncherClient {
}
public final void onStart() {
Log.i("FEED", "1");
if (!mDestroyed) {
Log.i("FEED", "2");
mLauncherService.setStopped(false);
reconnect();
mActivityState |= 1;
@@ -217,12 +217,7 @@ public class LauncherClient {
private void reconnect() {
if (!mDestroyed && (!mLauncherService.connect() || !mBaseService.connect())) {
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
setServiceState(0);
}
});
mActivity.runOnUiThread(() -> setServiceState(0));
}
}
@@ -1,4 +1,4 @@
package com.google.android.libraries.gsa.launcherclient;
package com.google.android.libraries.launcherclient;
import android.content.ComponentName;
import android.content.ServiceConnection;
@@ -0,0 +1,7 @@
package com.google.android.libraries.launcherclient;
public interface LauncherClientCallbacks {
void onOverlayScrollChanged(float f);
void onServiceStateChanged(boolean z, boolean z2);
}
@@ -1,10 +1,9 @@
package com.google.android.libraries.gsa.launcherclient;
package com.google.android.libraries.launcherclient;
import android.content.ComponentName;
import android.content.Context;
import android.os.IBinder;
import com.google.android.libraries.launcherclient.ILauncherOverlay;
import java.lang.ref.WeakReference;
public class LauncherClientService extends BaseClientService {
@@ -1,4 +1,4 @@
package com.google.android.libraries.gsa.launcherclient;
package com.google.android.libraries.launcherclient;
public class StaticInteger {
public final int mData;
+1 -1
View File
@@ -43,7 +43,7 @@
attributes and intent filters the same
-->
<activity
android:name="com.android.launcher3.uioverrides.QuickstepLauncher"
android:name="ch.deletescape.lawnchair.LawnchairLauncherQuickstep"
android:launchMode="singleTask"
android:clearTaskOnLaunch="true"
android:stateNotNeeded="true"