Search code examples
androidandroid-viewandroid-permissionslockscreenandroid-windowmanager

Android : Displaying view over the lockscreen (like Google Maps)


I would like to display a personalized view (who display some data in real time) on the lockscreen (after that the user locked the phone on the activity).
Google Maps et Baidu Maps (and some other apps that I forgot the name) have exactly implemented what I want.

So I'm trying to add a view when my BroadcastReceiver is triggered.
There is some points that I tried from this answer and this one. I tried this one too.
Before I got some error according to permission :

permission denied for window type 2XXX

Now I don't have any error but my view is not displayed.
There is my situation :
MainActivity.kt:

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity


class MainActivity : AppCompatActivity() {
    private val REQUEST_CODE = 10001

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        checkDrawOverlayPermission()
    }
    fun checkDrawOverlayPermission() {
        /** check if we already  have permission to draw over other apps */
        if (!Settings.canDrawOverlays(this)) {
            /** if not construct intent to request permission */
            val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                Uri.parse("package:" + getPackageName()));
            /** request permission via start activity for result */
            startActivityForResult(intent, REQUEST_CODE);
        }
        else {
            startService(Intent(this, LockScreenService::class.java))
        }
    }

    override fun onActivityResult(
        requestCode: Int,
        resultCode: Int,
        data: Intent?
    ) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_CODE) {
            if (Settings.canDrawOverlays(this)) {
                startService(Intent(this, LockScreenService::class.java))
            }
        }
    }
}

LockSceenService.kt:

class LockScreenService : Service() {
    private var mReceiver: BroadcastReceiver? = null
    private var isShowing = false
    override fun onBind(intent: Intent): IBinder? {
        // TODO Auto-generated method stub
        return null
    }

    private var windowManager: WindowManager? = null
    private var textview: TextView? = null
    private var mView: View? = null
    var params: WindowManager.LayoutParams? = null
    override fun onCreate() {
        super.onCreate()
        windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager

        mView = View.inflate(baseContext, R.layout.lockscreen_view, null)
        mView!!.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                or View.SYSTEM_UI_FLAG_FULLSCREEN
                or View.SYSTEM_UI_FLAG_VISIBLE
                or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
        mView!!.visibility = View.VISIBLE
        //set parameters for the textview

        val flag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        } else {
            WindowManager.LayoutParams.TYPE_PHONE
        }
        params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.WRAP_CONTENT,
            flag,
            WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                    or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
                    or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
                    or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                    or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
            PixelFormat.TRANSLUCENT
        )
        params!!.gravity = Gravity.BOTTOM

        //Register receiver for determining screen off and if user is present
        mReceiver = LockScreenStateReceiver()
        val filter = IntentFilter(Intent.ACTION_SCREEN_ON)
        filter.addAction(Intent.ACTION_USER_PRESENT)
        registerReceiver(mReceiver, filter)
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        return START_STICKY
    }

    inner class LockScreenStateReceiver : BroadcastReceiver() {
        override fun onReceive(
            context: Context,
            intent: Intent
        ) {
            if (intent.action == Intent.ACTION_SCREEN_ON) {
                //if screen is turn off show the textview
                if (!isShowing) {
                    windowManager!!.addView(mView, params)
                    isShowing = true
                }
            } else if (intent.action == Intent.ACTION_USER_PRESENT) {
                //Handle resuming events if user is present/screen is unlocked remove the textview immediately
                if (isShowing) {
                    windowManager!!.removeViewImmediate(textview)
                    isShowing = false
                }
            }
        }
    }

    override fun onDestroy() {
        //unregister receiver when the service is destroy
        if (mReceiver != null) {
            unregisterReceiver(mReceiver)
        }

        //remove view if it is showing and the service is destroy
        if (isShowing) {
            windowManager!!.removeViewImmediate(textview)
            isShowing = false
        }
        super.onDestroy()
    }
}

Manifest.xml:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.ACTION_MANAGE_OVERLAY_PERMISSION" />
<uses-permission android:name="android.permission.INTERNET" />

If you have any suggestions or other ways like "transparent activity over lock screen" (?) or just an upvote could be useful.

Thanks for your time!

EDIT: There is a video of what I want


Solution

  • You can show a view over the lock screen while the device is locked using foreground notification services, you would customize the default notification view with the custom XML notification layout and the foreground service has the capability to change the content of the view in real-time.

    In order to show the activity on the lock screen, you have to follow the below way

    Set these attributes in your activity which you want to show in the lock screen.

    Manifest file

    <activity
      android:name=".MainActivity"
      android:screenOrientation="fullSensor"
      android:showOnLockScreen="true">
    

    Add the below lines in your onCreate method of the activity

    getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON|
                WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD|
                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED|
                WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
    

    When your device is locked while the activity is open, it will remain in the lock screen with the back button so you can again navigate to the lock screen if needed.

    The working sample on the lock screen as below:

    Lock screen activity

    The sample notification from Google Maps and our customized notification alert

    Google Maps Sample Custom Sample

    You can follow the below Gist to create a custom notification layout with foreground service.

    Permissions

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    

    Manifest

    <service
        android:name=".MyForegroundService"
        android:icon="@drawable/ic_notification"
        android:label="MFS" />
    

    Service

    import android.app.Notification;
    import android.app.NotificationChannel;
    import android.app.NotificationManager;
    import android.app.PendingIntent;
    import android.app.Service;
    import android.content.Context;
    import android.content.Intent;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.os.Binder;
    import android.os.Build;
    import android.os.IBinder;
    import android.util.Log;
    import android.widget.RemoteViews;
    import android.widget.Toast;
    
    import androidx.core.app.NotificationCompat;
    
    public class MyForegroundService extends Service {
    
        private static final String TAG = "MyForegroundService";
    
        private static final int NOTIFICATION_ID = 2999;
        private static final String CHANNEL_ID = "MyForegroundService_ID";
        private static final CharSequence CHANNEL_NAME = "MyForegroundService Channel";
        private final IBinder mBinder = new LocalBinder();
    
        public class LocalBinder extends Binder {
            public MyForegroundService getService() {
                return MyForegroundService.this;
            }
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
            Log.d(TAG, "onCreate ");
            Toast.makeText(this, "The service is running", Toast.LENGTH_SHORT).show();
            startForeground(NOTIFICATION_ID, createNotification("The service is running"));
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            Log.d(TAG, "onStartCommand");
            return Service.START_STICKY;
        }
    
        private Notification createNotification(String message) {
    
            // Get the layouts to use in the custom notification
            RemoteViews notificationLayout = new RemoteViews(getPackageName(), R.layout.notification_main);
            notificationLayout.setTextViewText(R.id.txtTitle, message);
    
            NotificationManager mNotificationManager;
            NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, CHANNEL_ID);
    
            Intent notificationIntent = new Intent(this, MainActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 125, notificationIntent, 0);
    
            Bitmap payableLogo = BitmapFactory.decodeResource(getResources(), R.drawable.ic_notification);
    
            mBuilder.setContentTitle("My Service")
                    .setContentText(message)
                    .setPriority(Notification.PRIORITY_HIGH)
                    .setLargeIcon(payableLogo)
                    .setSmallIcon(R.drawable.ic_notification)
                    .setContentIntent(pendingIntent)
                    .setAutoCancel(false)
    
                    .setCustomBigContentView(notificationLayout);
    
            mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                String channelId = CHANNEL_ID;
                NotificationChannel channel = new NotificationChannel(channelId, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
                mNotificationManager.createNotificationChannel(channel);
                mBuilder.setChannelId(channelId);
            }
    
            return mBuilder.build();
        }
    
        private void showNotification(String message) {
            NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            mNotificationManager.notify(NOTIFICATION_ID, createNotification(message));
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            return mBinder;
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
            Log.d(TAG, "onDestroy");
        }
    }
    

    Notification Layout

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimaryDark"
        android:orientation="vertical">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:paddingHorizontal="20dp"
            android:paddingVertical="15dp">
    
            <TextView
                android:id="@+id/txtTitle"
                style="@style/TextAppearance.Compat.Notification.Info.Media"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="This is from my service"
                android:textColor="#fff" />
    
            <TextView
                android:id="@+id/txtResult"
                style="@style/TextAppearance.Compat.Notification.Title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="5dp"
                android:text="Any text goes here"
                android:textColor="#fff" />
    
        </LinearLayout>
    
    </LinearLayout>
    

    Reference: https://gist.github.com/aslamanver/f32a0bb8461c250d4a945e11f6771456