📜 ⬆️ ⬇️

As I wrote a custom locker



Hi to habrastarezilam from habranovichka. Exactly a year ago, I decided to write a custom locker (lock screen) for my old Samsung Galaxy Gio in the style of the then popular Samsung Galaxy s3. What reasons have forced me to do this, I will not write, but I will add only that I did not intend to upload the program on Google Play and did not plan to make money on it in any other way. This post is dedicated to the consequences of my decision.


I'll start from afar. Many praise Android for its openness and the ability to replace and customize firmware to fit their needs. What is there to say? Compared to other popular operating systems, this is certainly true, but if you dig deeper into the Android architecture, difficulties and questions arise. Lockscreen (in Android, this is called keyguard) just raises questions: why Google did not do with it, because with launchers, why didn’t they make a dialogue with all the lockers available on the device and with the option to choose the default one? Somewhere in the depths of the brain someone answers in a low indecisive voice: maybe Google (Android Ink. To be more precise) did so for security reasons. This voice is probably right and many locker developers and me (modesty did not allow to attribute themselves to them) had to reinvent the wheel, and not one.
')

We study the source code


I started using one of the advantages of Android - from exploring the sources. I am one of those conservatives who have been sitting on the stock firmware for 2.5 years (2.3.6), therefore I studied the relevant sources. The classes responsible for locking the screen are in android.policy.jar, which is in the system / framework. The original goal was to find the “entry point”, i.e. where and when the locker is called. Searched here .

The PhoneWindowManager.java class has a screenTurnedOff (int why) method that calls the KeyguardViewMediator class method of the same name. Having traced who is calling whom, I found a method in the KeyguardViewManager class that creates the View Locker directly.

public synchronized void show() { if (DEBUG) Log.d(TAG, "show(); mKeyguardView==" + mKeyguardView); if (mKeyguardHost == null) { if (DEBUG) Log.d(TAG, "keyguard host is null, creating it..."); mKeyguardHost = new KeyguardViewHost(mContext, mCallback); final int stretch = ViewGroup.LayoutParams.MATCH_PARENT; int flags = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN | WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER | WindowManager.LayoutParams.FLAG_KEEP_SURFACE_WHILE_ANIMATING /*| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR*/ ; if (!mNeedsInput) { flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; } WindowManager.LayoutParams lp = new WindowManager.LayoutParams( stretch, stretch, WindowManager.LayoutParams.TYPE_KEYGUARD, flags, PixelFormat.TRANSLUCENT); lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN; lp.windowAnimations = com.android.internal.R.style.Animation_LockScreen; lp.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; lp.setTitle("Keyguard"); mWindowLayoutParams = lp; mViewManager.addView(mKeyguardHost, lp); } if (mKeyguardView == null) { if (DEBUG) Log.d(TAG, "keyguard view is null, creating it..."); mKeyguardView = mKeyguardViewProperties.createKeyguardView(mContext, mUpdateMonitor, this); mKeyguardView.setId(R.id.lock_screen); mKeyguardView.setCallback(mCallback); final ViewGroup.LayoutParams lp = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); mKeyguardHost.addView(mKeyguardView, lp); if (mScreenOn) { mKeyguardView.onScreenTurnedOn(); } } mKeyguardHost.setVisibility(View.VISIBLE); mKeyguardView.requestFocus(); } 

Well, all ingenious is simple. I decided to repeat this code for my application and got an error - there is no necessary permission. A little googling added the following permissions : SYSTEM_ALERT_WINDOW and INTERNAL_SYSTEM_WINDOW. It did not help.

Returned to the study of the class PhoneWindowManager.java:
 public int checkAddPermission(WindowManager.LayoutParams attrs) { int type = attrs.type; if (type < WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW || type > WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) { return WindowManagerImpl.ADD_OKAY; } String permission = null; switch (type) { case TYPE_TOAST: // XXX right now the app process has complete control over // this... should introduce a token to let the system // monitor/control what they are doing. break; case TYPE_INPUT_METHOD: case TYPE_WALLPAPER: // The window manager will check these. break; case TYPE_PHONE: case TYPE_PRIORITY_PHONE: case TYPE_SYSTEM_ALERT: case TYPE_SYSTEM_ERROR: case TYPE_SYSTEM_OVERLAY: permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW; break; default: permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW; } if (permission != null) { if (mContext.checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { return WindowManagerImpl.ADD_PERMISSION_DENIED; } } return WindowManagerImpl.ADD_OKAY; } 

For the required TYPE_KEYGUARD window, you need the second of my added permissions. Back end of the body began to feel that not everything is as simple as I imagined. It was decided to look at the description of this permission. Here is an excerpt from the AndroidManifest.xml package framework-res.apk.

 <permission android:label="@string/permlab_internalSystemWindow" android:name="android.permission.INTERNAL_SYSTEM_WINDOW" android:protectionLevel="signature" android:description="@string/permdesc_internalSystemWindow" /> 

Here it is - a black stripe in life. After all, I understood “signature” - this means that only a package signed with the same key as the package that issued this permission (in our case, framework-res.apk) can use this perm. Okay, we get the tools for making bicycles.

Version one


The first solution was to use activity as loxgrin. On stackoverflow, it is advised to use the following code:

 @Override public void onAttachedToWindow(){ getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD); super.onAttachedToWindow(); } 

I admit, in the first versions I used this method. It has significant drawbacks: the statusbar is not blocked, since version API11 this method does not work.

The solution to the first flaw ( overflow of the stack again helped) the following. A transparent View is drawn on top of the statusbar using WindowManager , which intercepts all TouchEvents. Here is a service that implements this:

 public class StatusbarService extends Service { View v; @Override public void onStart(Intent intent, int id) { super.onStart(intent, id); Bundle e = intent.getExtras(); if(e != null){ int statusBarHeight = (Integer) e.get("SBH"); WindowManager.LayoutParams lp = new WindowManager.LayoutParams(WindowManager.LayoutParams.FILL_PARENT, statusBarHeight, WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT); lp.gravity = Gravity.TOP; WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); v = new View(getBaseContext()); wm.addView(v, lp); } } @Override public void onDestroy() { super.onDestroy(); WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); wm.removeView(v); } @Override public IBinder onBind(Intent arg0) { return null; } } statusBarHeight, WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT); public class StatusbarService extends Service { View v; @Override public void onStart(Intent intent, int id) { super.onStart(intent, id); Bundle e = intent.getExtras(); if(e != null){ int statusBarHeight = (Integer) e.get("SBH"); WindowManager.LayoutParams lp = new WindowManager.LayoutParams(WindowManager.LayoutParams.FILL_PARENT, statusBarHeight, WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT); lp.gravity = Gravity.TOP; WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); v = new View(getBaseContext()); wm.addView(v, lp); } } @Override public void onDestroy() { super.onDestroy(); WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); wm.removeView(v); } @Override public IBinder onBind(Intent arg0) { return null; } } 

There was no second drawback for me, this code worked perfectly on Gingerbread. On w3bsit3-dns.com, where I recklessly laid out my creation, users complained that on many phones my locker rolled up like a normal application. For them, found such a solution. A dummy is installed as a standard launcher. When I press the HOME button, the system calls up my empty-launcher. If the custom locker is active, the launcher is immediately closed in the onCreate () method, i.e. Visually pressing the HOME button leads to nothing. If the custom locker is not active, my launcher immediately calls another correct launcher, which the user has specified in the settings.

Here is the dummy code:
 public class HomeActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(MainService.unlocked != false){ try{ SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); String pn = pref.getString("settings_launcher_pn", ""); String an = pref.getString("settings_launcher_an", ""); Intent launch = new Intent(Intent.ACTION_MAIN); launch.addCategory(Intent.CATEGORY_HOME); launch.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); launch.setClassName(pn, an); startActivity(launch); } catch(Exception e){ Intent i = null; PackageManager pm = getPackageManager(); for(ResolveInfo ri:pm.queryIntentActivities(new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME), PackageManager.MATCH_DEFAULT_ONLY)){ if(!getPackageName().equals(ri.activityInfo.packageName)){ i = new Intent().addCategory(Intent.CATEGORY_HOME).setAction(Intent.ACTION_MAIN).addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS).setClassName(ri.activityInfo.packageName, ri.activityInfo.name); } } if(i != null) startActivity(i); } } finish(); } } 

It looked like this:


These bikes drove long and well, until I decided to make the "correct" loxgreen, and already in the style of the Samsung Galaxy S4.

Version two


When does the system need to run a custom locker? Obviously, when you turn off the screen. Create a service registering BroadcastReceiver, because from the manifest, this filter does not work.

It is necessary to consider two features:

1. The service must be started at the time of loading the device. Create a BroadcastReseiver with an IntentFilter "android.intent.action.BOOT_COMPLETED." There is one BUT: the service at startup should disable the standard screen lock. A feature of Android is that the standard PIN entry window is part of the stock lock screen. Therefore, the service should only be started when a PIN is entered.

The maximum that was enough for my imagination:
 public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); if(PreferenceManager.getDefaultSharedPreferences(context).getBoolean("unlock_screen_enable", false)){ if(tm.getSimState() != TelephonyManager.SIM_STATE_PIN_REQUIRED && tm.getSimState() != TelephonyManager.SIM_STATE_PUK_REQUIRED){ context.startService(new Intent(context, KeyguardService.class)); } else { AlarmManager alarms = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); Intent intentToFire = new Intent(context, BootReceiver.class); PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0, intentToFire, 0); alarms.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 10000, alarmIntent); } } } } 

2. After analyzing PhoneWindowManager, you can see that the screenTurnedOff (int why) method passes the variable why, which takes 3 values:
- the screen turned off after a timeout (in this case, the stock locker starts with a delay),
- the screen turned off when the proximity sensor was triggered (during a telephone conversation),
- The screen turns off when you press a button.
In my case, there is no such diversity. Therefore, the service monitors the status of the phone, and when an incoming call or during a call, the screen is not blocked.

Here is the main service code:
 public class KeyguardService extends Service { KeyguardMediator keyguardMediator; KeyguardManager.KeyguardLock keyguardLock; boolean telephone = false; //false - no call, true - in call boolean wasLocked = false; @Override public void onCreate() { super.onCreate(); TelephonyManager telephonyManager = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE); telephonyManager.listen(new MyPhoneStateListener(), PhoneStateListener.LISTEN_CALL_STATE); keyguardLock = ((KeyguardManager)getSystemService(KEYGUARD_SERVICE)).newKeyguardLock("Custom keyguard by Arriva"); keyguardLock.disableKeyguard(); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_OFF); registerReceiver(receiver, filter); keyguardMediator = new KeyguardMediator(this); } @Override public void onDestroy() { super.onDestroy(); unregisterReceiver(receiver); keyguardLock.reenableKeyguard(); keyguardLock = null; keyguardMediator.destroy(); } void changeTelephoneState(int state){ if(state == TelephonyManager.CALL_STATE_IDLE){ telephone = false; if(wasLocked){ wasLocked = false; keyguardMediator.visibility(true); } } else { telephone = true; if(keyguardMediator.isShowing){ wasLocked = true; keyguardMediator.visibility(false); } } } private BroadcastReceiver receiver = new BroadcastReceiver(){ @Override public void onReceive(Context context, Intent intent) { String settingsLock = PreferenceManager.getDefaultSharedPreferences(context).getString("screen_lock", "2"); if(!settingsLock.equals("1")){ keyguardMediator.show(); } } }; class MyPhoneStateListener extends PhoneStateListener { @Override public void onCallStateChanged(int state, String incomingNumber){ super.onCallStateChanged(state, incomingNumber); changeTelephoneState(state); } } } 

The idea of ​​not using activity, but using WindowManager was still strong. Of the five types of windows that use the SYSTEM_ALERT_WINDOW permission, I was approached by TYPE_SYSTEM_ALERT. Moreover, he had obvious advantages: the status bar was blocked (at least on Gingerbread) and the HOME button was intercepted (it even works on Jelly Bean).

The intermediary between the service and KeyguardView is the KeyguardMediator class:
 public class KeyguardMediator { WindowManager windowManager; KeyguardHost keyguardHost; KeyguardView keyguardView; Context context; boolean isShowing; String[] prefShortcutsArray; String prefScreenLock; String prefUnlockEffect; String prefPatternPassword; boolean prefMultipleWidgets; boolean prefShortcuts; boolean prefHelpText; boolean prefPatternVisible; boolean prefWallpaper; boolean drawWallpaperView; boolean drawWallpaperViewSqueeze; public KeyguardMediator(Context con){ context = con; windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); //          } void onResume(){ if(keyguardView != null){ keyguardView.onResume(); } } void onPause(){ if(keyguardView != null){ keyguardView.onPause(); } } void show(){ if(isShowing){ visibility(true); return; } keyguardView = new KeyguardView(context, this); isShowing = true; int flags = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN; if(!drawWallpaperView) { flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; } int format = PixelFormat.OPAQUE; if(!drawWallpaperView) { format = PixelFormat.TRANSLUCENT; } WindowManager.LayoutParams lp = new WindowManager.LayoutParams(WindowManager.LayoutParams.FILL_PARENT, WindowManager.LayoutParams.FILL_PARENT, WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, flags, format); if(drawWallpaperView){ lp.windowAnimations = android.R.style.Animation_Toast; //      } lp.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; lp.setTitle("Custom keyguard"); keyguardHost = new KeyguardHost(context); keyguardHost.addView(keyguardView); windowManager.addView(keyguardHost, lp); } void hide(){ if(!isShowing){ return; } isShowing = false; keyguardHost.setVisibility(View.GONE); //    View    ,      windowManager.removeView(keyguardHost); keyguardHost = null; keyguardView = null; } void visibility(boolean visible){ //    View   keyguardHost.setVisibility(visible ? View.VISIBLE : View.GONE); if(keyguardView != null){ if(visible){ keyguardView.onResume(); } else { keyguardView.onPause(); } } } void startWidgetPicker(){ //  activity   } void finishWidgetPicker(){ //   layout'   } void destroy(){ if(keyguardHost != null){ windowManager.removeView(keyguardHost); keyguardHost = null; keyguardView = null; } } } 

Then the story becomes less interesting, so to speak, everyday. You can add application shortcuts to my locker (everything is standard and simple here) and widgets (but this moment is worthy of a separate article).

Now everything began to look more modern:




Conclusion


With this post, I did not want to promote myself. This is not a guide for writing lockers. I just wanted to show how a person who was too lazy to read at least one book on the basics of Java, but practicing writing programs for two years, can contrive to get a specific result.

Source: https://habr.com/ru/post/220689/


All Articles